From da49146355607d1f037f6a7ea21692351aee0336 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 11 Aug 2026 09:25:48 +0530 Subject: [PATCH 01/16] SK-3043 refactor code for common code separation --- .github/workflows/beta-release.yml | 6 +- .github/workflows/bump_version.yml | 6 +- .github/workflows/internal_release.yml | 8 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 6 +- Package.swift | 47 +- .../ExampleAPICallback.swift | 48 -- .../ResponseStructs.swift | 23 - .../ExampleAPICallback.swift | 48 -- .../ComposableElements/ResponseStructs.swift | 23 - .../GetSample.xcodeproj/project.pbxproj | 640 --------------- .../xcshareddata/swiftpm/Package.resolved | 23 - Samples/GetSample/GetSample/AppDelegate.swift | 36 - .../GetSample/ExampleAPICallback.swift | 50 -- .../GetSample/ExampleTokenProvider.swift | 31 - .../GetSample/GetSample/ResponseStructs.swift | 23 - .../GetSample/GetSample/SceneDelegate.swift | 52 -- .../GetSample/GetSample/ViewController.swift | 112 --- .../GetSampleTests/GetSampleTests.swift | 36 - .../GetSampleUITests/GetSampleUITests.swift | 41 - .../GetSampleUITestsLaunchTests.swift | 32 - Samples/GetSample/Podfile | 12 - .../AppIcon.appiconset/Contents.json | 13 - .../InputFormatting/ExampleAPICallback.swift | 50 -- .../InputFormatting/Info.plist | 25 - .../InputFormatting/ResponseStructs.swift | 29 - Samples/README.md | 32 +- .../project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 15 + .../CollectAndRevealSample/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../CardBrandChoiceSampleViewController.swift | 2 +- .../CollectAndRevealViewController.swift | 111 ++- .../ExampleAPICallback.swift | 7 + .../ExampleTokenProvider.swift | 0 .../CollectAndRevealSample/Info.plist | 0 .../ResponseStructs.swift | 7 + .../SceneDelegate.swift | 0 .../ViewController.swift | 0 .../CollectAndRevealSample/Podfile | 3 +- .../CollectAndRevealSample/README.md | 0 .../project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../ComposableElements/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../ExampleAPICallback.swift | 7 + .../ExampleTokenProvider.swift | 0 .../ComposableElements/Info.plist | 0 .../ComposableElements/ResponseStructs.swift | 7 + .../ComposableElements/SceneDelegate.swift | 0 .../ComposableElements/Untitled.swift | 0 .../ComposableElements/ViewController.swift | 104 ++- .../ComposableElementsTests.swift | 0 .../ComposableElementsUITests.swift | 0 ...ComposableElementsUITestsLaunchTests.swift | 0 .../{ => Skyflow}/ComposableElements/Podfile | 3 +- .../InputFormatting.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../InputFormatting/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../InputFormatting/ExampleAPICallback.swift | 7 + .../ExampleTokenProvider.swift | 0 .../InputFormatting}/Info.plist | 0 .../InputFormatting/ResponseStructs.swift | 7 + .../InputFormatting/SceneDelegate.swift | 0 .../InputFormatting/ViewController.swift | 60 +- .../InputFormattingTests.swift | 0 .../InputFormattingUITests.swift | 0 .../InputFormattingUITestsLaunchTests.swift | 0 Samples/{ => Skyflow}/InputFormatting/Podfile | 3 +- .../InputFormatting/Podfile.lock | 4 - .../UpsertFeature.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 9 - .../UpsertFeature/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../UpsertFeature}/Base.lproj/Main.storyboard | 0 .../UpsertFeature/UpsertFeature/Info.plist | 0 .../UpsertFeature/ResponseStructs.swift | 7 + .../UpsertFeature/SceneDelegate.swift | 0 .../UpsertFeature/ViewController.swift | 73 +- Samples/{ => Skyflow}/Validations/Podfile | 3 +- Samples/{ => Skyflow}/Validations/README.md | 0 .../Validations.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../Validations/Validations/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Validations}/Base.lproj/Main.storyboard | 0 .../Validations/ExampleAPICallback.swift | 0 .../Validations/ExampleTokenProvider.swift | 0 .../Validations/Validations/Info.plist | 0 .../Validations/SceneDelegate.swift | 0 .../Validations/ViewController.swift | 0 Samples/UpdateDataUsingElements/Podfile | 26 - .../project.pbxproj | 756 ------------------ .../contents.xcworkspacedata | 10 - .../UpdateDataUsingElements/AppDelegate.swift | 36 - .../AppIcon.appiconset/Contents.json | 35 - .../ExampleAPICallback.swift | 48 -- .../ExampleTokenProvider.swift | 31 - .../UpdateDataUsingElements/Info.plist | 25 - .../ResponseStructs.swift | 24 - .../SceneDelegate.swift | 52 -- .../ViewController.swift | 183 ----- .../UpdateDataUsingElementsTests.swift | 36 - .../UpdateDataUsingElementsUITests.swift | 41 - ...eDataUsingElementsUITestsLaunchTests.swift | 33 - .../AccentColor.colorset/Contents.json | 11 - .../Base.lproj/LaunchScreen.storyboard | 25 - .../UpsertFeature/Base.lproj/Main.storyboard | 24 - .../UpsertFeature/ResponseStructs.swift | 20 - .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../AccentColor.colorset/Contents.json | 11 - .../Validations/Assets.xcassets/Contents.json | 6 - .../Base.lproj/LaunchScreen.storyboard | 25 - .../Validations/Base.lproj/Main.storyboard | 24 - Skyflow-flowvault-ios.podspec | 46 ++ Skyflow.podspec | 21 +- Sources/Skyflow/Exports.swift | 7 + Sources/Skyflow/{core => }/Init.swift | 1 + .../Resources/Assets.xcassets/Contents.json | 6 - Sources/Skyflow/Version.swift | 16 - .../Skyflow/collect/CollectAPICallback.swift | 4 +- ...swift => CollectContainerOperations.swift} | 26 +- .../Skyflow/collect/CollectRequestBody.swift | 2 +- .../ComposableContainerOperations.swift | 139 ++++ .../Skyflow/collect/InsertAPICallback.swift | 2 +- ...{APIClient.swift => APIClientLegacy.swift} | 74 +- Sources/Skyflow/core/Client.swift | 431 +--------- Sources/Skyflow/core/ClientOperations.swift | 377 +++++++++ Sources/Skyflow/core/Configuration.swift | 14 +- Sources/Skyflow/core/ContainerOptions.swift | 22 + Sources/Skyflow/core/ContextOptions.swift | 24 - .../Skyflow/core/container/Container.swift | 27 - .../core/container/ContainerOptions.swift | 29 - .../elements/CollectElementInput.swift | 38 + .../elements/CollectElementOptions.swift | 26 + .../elements/core/CollectElementInput.swift | 69 -- Sources/Skyflow/reveal/GetByIdRecord.swift | 4 +- Sources/Skyflow/reveal/GetRecord.swift | 8 +- ....swift => RevealContainerOperations.swift} | 28 +- .../Skyflow/reveal/RevealElementInput.swift | 28 +- .../Skyflow/reveal/RevealElementOptions.swift | 24 +- Sources/Skyflow/skyflow_iOS.swift | 7 - .../BaseElement.swift | 0 .../Amex-Card.imageset/Amex-Card@3x.png | Bin .../Amex-Card.imageset/Contents.json | 0 .../Cartes-Bancaires-Card.png | Bin .../Contents.json | 0 .../Resources}/Assets.xcassets/Contents.json | 0 .../Copy-Icon.imageset/Contents.json | 0 .../Copy-Icon.imageset/Copy-Icon.png | Bin .../Diners-Card.imageset/Contents.json | 0 .../Diners-Card.imageset/Diners-Card@3x.png | Bin .../Discover-Card.imageset/Contents.json | 0 .../Discover-Card@3x.png | Bin .../Hipercard-Card.imageset/Contents.json | 0 .../Hipercard-Card@3x.png | Bin .../JCB-Card.imageset/Contents.json | 0 .../JCB-Card.imageset/JCB-Card@3x.png | Bin .../Maestro-Card.imageset/Contents.json | 0 .../Maestro-Card.imageset/Maestro-Card@3x.png | Bin .../Mastercard-Card.imageset/Contents.json | 0 .../Mastercard-Card@3x.png | Bin .../Success-Icon.imageset/Contents.json | 0 .../Success-Icon.imageset/Success-Icon.png | Bin .../Unionpay-Card.imageset/Contents.json | 0 .../Unionpay-Card@3x.png | Bin .../Unknown-Card.imageset/Contents.json | 0 .../Unknown-Card.imageset/Unknown-Card@3x.png | Bin .../Visa-Card.imageset/Contents.json | 0 .../Visa-Card.imageset/Group 82.png | Bin .../checkmark.imageset/Contents.json | 0 .../checkmark.imageset/checkmark.png | Bin .../dropdown.imageset/Contents.json | 0 .../dropdown.imageset/dropdown.png | Bin Sources/SkyflowCore/Version.swift | 23 + .../collect/CollectContainer.swift | 26 + .../collect/TokenAPICallback.swift | 0 .../composable/AutoshiftAllowed.swift | 0 .../composable/ComposableContainer.swift | 160 +--- Sources/SkyflowCore/core/APIClient.swift | 66 ++ .../SkyflowCore/core/BaseConfiguration.swift | 24 + Sources/SkyflowCore/core/ClientBase.swift | 81 ++ Sources/SkyflowCore/core/ContextOptions.swift | 24 + .../core/Element.swift | 0 .../{Skyflow => SkyflowCore}/core/Env.swift | 0 .../core/EventName.swift | 0 .../core/InterfaceName.swift | 6 +- .../{Skyflow => SkyflowCore}/core/Log.swift | 10 +- .../core/LogLevel.swift | 0 .../core/Message.swift | 4 +- .../core/Options.swift | 0 .../core/container/BaseContainerOptions.swift | 26 + .../core/container/Container.swift | 27 + .../core/container/ContainerProtocol.swift | 0 .../core/container/ContainerType.swift | 0 .../core/protocols/Callback.swift | 0 .../core/protocols/TokenProvider.swift | 0 .../elements/FormatTextField.swift | 8 +- .../elements/PaddingLabel.swift | 8 +- .../elements/SkyflowElement.swift | 26 +- .../elements/TextField.swift | 46 +- .../TextFieldValidationDelegate.swift | 0 .../core/BaseCollectElementInput.swift | 40 + .../core/BaseCollectElementOptions.swift} | 29 +- .../elements/core/ElementType.swift | 4 +- .../elements/core/state/State.swift | 4 +- .../elements/core/state/StateforText.swift | 2 +- .../elements/core/styles/Style.swift | 0 .../elements/core/styles/Styles.swift | 0 .../elements/utils/CardBin.swift | 0 .../elements/utils/CardType.swift | 0 .../elements/utils/CharacterSet.swift | 0 .../validations/ElementValueMatchRule.swift | 0 .../validations/LengthMatchRule.swift | 0 .../elements/validations/RegexMatchRule.swift | 0 .../SkyflowInternalValidatorProtocol.swift | 0 .../SkyflowValidateCardExpirationDate.swift | 0 .../SkyflowValidateCardNumber.swift | 0 .../SkyflowValidateExpirationMonth.swift | 0 .../SkyflowValidateExpirationYear.swift | 0 .../SkyflowValidateLengthMatch.swift | 0 .../validations/SkyflowValidationError.swift | 0 .../validations/SkyflowValidator.swift | 0 .../elements/validations/ValidationRule.swift | 0 .../elements/validations/ValidationSet.swift | 2 +- .../errors/ErrorCodes.swift | 115 ++- .../reveal/BaseRevealElementInput.swift | 34 + .../reveal/BaseRevealElementOptions.swift | 33 + .../reveal/ConversionHelpers.swift | 4 +- .../reveal/FormatLabel.swift | 0 .../reveal/Label.swift | 24 +- .../reveal/RedactionType.swift | 0 .../SkyflowCore/reveal/RevealContainer.swift | 26 + .../reveal/RevealErrorRecord.swift | 8 +- .../reveal/RevealSuccessRecord.swift | 8 +- .../reveal/SkyflowLabelView.swift | 6 +- .../reveal/UILabel+Copyable.swift | 6 +- .../utils/DictionaryExtension.swift | 2 +- .../utils/FetchMetrices.swift | 9 +- .../utils/SkyflowAssetBundle.swift | 14 + .../utils/StringExtension.swift | 3 - Sources/SkyflowFlowVaultIOS/Exports.swift | 7 + Sources/SkyflowFlowVaultIOS/Init.swift | 12 + .../collect/AdditionalFields.swift | 29 + .../collect/CVVMockGenerator.swift | 30 + .../collect/CVVTokenReplacer.swift | 132 +++ .../collect/CollectContainerOperations.swift | 112 +++ .../collect/CollectOptions.swift | 16 + .../collect/CollectResponse.swift | 69 ++ .../ComposableContainerOperations.swift | 116 +++ .../collect/FlowVaultCollectAPICallback.swift | 292 +++++++ .../collect/FlowVaultCollectRequestBody.swift | 205 +++++ .../collect/FlowVaultICOptions.swift | 46 ++ .../collect/FlowVaultInsertAPICallback.swift | 295 +++++++ .../collect/FlowVaultInsertRequestBody.swift | 29 + .../collect/FlowVaultUpdateRequestBody.swift | 13 + .../collect/InsertOptions.swift | 14 + .../collect/UpsertOption.swift | 24 + .../core/APIClientFlowVault.swift | 23 + Sources/SkyflowFlowVaultIOS/core/Client.swift | 22 + .../core/ClientOperations.swift | 138 ++++ .../core/Configuration.swift | 17 + .../core/ContainerOptions.swift | 22 + .../elements/CollectElementInput.swift | 38 + .../elements/CollectElementOptions.swift | 26 + .../FlowVaultDetokenizeRequestBody.swift | 20 + .../reveal/FlowVaultRevealApiCallback.swift | 157 ++++ .../reveal/RevealContainerOperations.swift | 81 ++ .../reveal/RevealElementInput.swift | 17 + .../reveal/RevealElementOptions.swift | 16 + .../reveal/RevealOptions.swift | 20 + .../reveal/RevealRequestBody.swift | 20 + .../reveal/RevealRequestRecord.swift | 14 + .../reveal/RevealResponse.swift | 88 ++ .../reveal/RevealValueCallback.swift | 148 ++++ .../reveal/TokenGroupRedaction.swift | 17 + Tests/skyflow-iOS-collectTests/DemoImpl.swift | 26 +- .../skyflow_iOS_collectTests.swift | 332 +++----- ...ow_iOS_emptySkyflowIdValidationTests.swift | 104 +++ .../DemoImpl.swift | 3 +- ...skyflow_iOS_composableEelementsTests.swift | 74 +- .../CollectElementOptionsTests.swift | 29 +- Tests/skyflow-iOS-elementTests/DemoImpl.swift | 3 +- .../InputFormattingTests.swift | 47 +- .../TextFieldDelegateTests.swift | 17 +- .../skyflow_iOS_containerTests.swift | 3 +- .../skyflow_iOS_elementTests.swift | 65 +- Tests/skyflow-iOS-errorTests/DemoImpl.swift | 22 +- .../skyflow-iOS-errorTests/GatewayImpl.swift | 3 +- .../Skyflow_iOS_collectErrorTests.swift | 464 +++++++---- .../Skyflow_iOS_generalErrorTests.swift | 70 +- .../Skyflow_iOS_revealErrorTests.swift | 112 +-- ...low_iOS_revealOptionsValidationTests.swift | 82 ++ Tests/skyflow-iOS-getByIdTests/DemoImpl.swift | 1 + .../skyflow_iOS_getByIdTests.swift | 22 +- .../skyflow_iOS_getByIdUtilTests.swift | 1 + Tests/skyflow-iOS-getTests/DemoImpl.swift | 1 + .../skyflow_iOS_getTests.swift | 27 +- .../skyflow_iOS_getUtilTests.swift | 1 + .../skyflow_iOS_validateGetRecordTest.swift | 1 + Tests/skyflow-iOS-legacyTests/DemoImpl.swift | 42 + .../MockURLProtocol.swift | 41 + ...w_iOS_dormantCollectApiCallbackTests.swift | 250 ++++++ ...w_iOS_dormantCollectRequestBodyTests.swift | 103 +++ .../skyflow_iOS_legacyUtilTests.swift | 134 ++++ .../skyflow_iOS_revealApiCallbackTests.swift | 179 +++++ Tests/skyflow-iOS-revealTests/DemoImpl.swift | 26 +- .../RevealElementOptionsTests.swift | 31 +- .../RevealSkyflowLabelViewTests.swift | 39 +- .../skyflow_iOS_revealTests.swift | 56 +- .../ClientScenario.swift | 7 +- .../skyflow-iOS-scenarioTests/DemoImpl.swift | 5 +- .../DetokenizeScenario.swift | 1 + .../GetByIdScenario.swift | 1 + .../InsertScenario.swift | 1 + .../skyflow_iOS_detokenizeScenarioTests.swift | 1 + .../skyflow_iOS_getByIdScenarioTests.swift | 1 + .../skyflow_iOS_insertScenarioTests.swift | 1 + Tests/skyflow-iOS-utilTests/DemoImpl.swift | 22 +- .../MockURLProtocol.swift | 41 + .../skyflow_iOS_collectUtilTests.swift | 751 +++++++++-------- .../skyflow_iOS_cvvMockTests.swift | 370 +++++++++ .../skyflow_iOS_insertUtilTests 2.swift | 233 ++++-- .../skyflow_iOS_revealUtilTests.swift | 528 +++++++++--- .../skyflow_iOS_utilTests.swift | 68 +- scripts/bump_version.sh | 35 +- 357 files changed, 7365 insertions(+), 5265 deletions(-) delete mode 100644 Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift delete mode 100644 Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift delete mode 100644 Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift delete mode 100644 Samples/ComposableElements/ComposableElements/ResponseStructs.swift delete mode 100644 Samples/GetSample/GetSample.xcodeproj/project.pbxproj delete mode 100644 Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Samples/GetSample/GetSample/AppDelegate.swift delete mode 100644 Samples/GetSample/GetSample/ExampleAPICallback.swift delete mode 100644 Samples/GetSample/GetSample/ExampleTokenProvider.swift delete mode 100644 Samples/GetSample/GetSample/ResponseStructs.swift delete mode 100644 Samples/GetSample/GetSample/SceneDelegate.swift delete mode 100644 Samples/GetSample/GetSample/ViewController.swift delete mode 100644 Samples/GetSample/GetSampleTests/GetSampleTests.swift delete mode 100644 Samples/GetSample/GetSampleUITests/GetSampleUITests.swift delete mode 100644 Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift delete mode 100644 Samples/GetSample/Podfile delete mode 100644 Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift delete mode 100644 Samples/InputFormatting/InputFormatting/Info.plist delete mode 100644 Samples/InputFormatting/InputFormatting/ResponseStructs.swift rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift (97%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift (68%) create mode 100644 Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/Info.plist (100%) create mode 100644 Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift (100%) rename Samples/{ => Skyflow}/CollectAndRevealSample/Podfile (81%) rename Samples/{ => Skyflow}/CollectAndRevealSample/README.md (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements.xcodeproj/project.pbxproj (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/AppDelegate.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Assets.xcassets/Contents.json (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Base.lproj/Main.storyboard (100%) create mode 100644 Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/ExampleTokenProvider.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Info.plist (100%) create mode 100644 Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/SceneDelegate.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/Untitled.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElements/ViewController.swift (68%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift (100%) rename Samples/{ => Skyflow}/ComposableElements/Podfile (88%) rename Samples/{ => Skyflow}/InputFormatting/InputFormatting.xcodeproj/project.pbxproj (100%) rename Samples/{GetSample/GetSample.xcodeproj => Skyflow/InputFormatting/InputFormatting.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{GetSample/GetSample.xcodeproj => Skyflow/InputFormatting/InputFormatting.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Samples/{ => Skyflow}/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{InputFormatting/InputFormatting.xcodeproj/project.xcworkspace => Skyflow/InputFormatting/InputFormatting.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Samples/{ => Skyflow}/InputFormatting/InputFormatting/AppDelegate.swift (100%) rename Samples/{GetSample/GetSample => Skyflow/InputFormatting/InputFormatting}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Samples/{GetSample/GetSample => Skyflow/InputFormatting/InputFormatting}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Samples/{GetSample/GetSample => Skyflow/InputFormatting/InputFormatting}/Assets.xcassets/Contents.json (100%) rename Samples/{GetSample/GetSample => Skyflow/InputFormatting/InputFormatting}/Base.lproj/LaunchScreen.storyboard (100%) rename Samples/{GetSample/GetSample => Skyflow/InputFormatting/InputFormatting}/Base.lproj/Main.storyboard (100%) create mode 100644 Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift rename Samples/{ => Skyflow}/InputFormatting/InputFormatting/ExampleTokenProvider.swift (100%) rename Samples/{GetSample/GetSample => Skyflow/InputFormatting/InputFormatting}/Info.plist (100%) create mode 100644 Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift rename Samples/{ => Skyflow}/InputFormatting/InputFormatting/SceneDelegate.swift (100%) rename Samples/{ => Skyflow}/InputFormatting/InputFormatting/ViewController.swift (86%) rename Samples/{ => Skyflow}/InputFormatting/InputFormattingTests/InputFormattingTests.swift (100%) rename Samples/{ => Skyflow}/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift (100%) rename Samples/{ => Skyflow}/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift (100%) rename Samples/{ => Skyflow}/InputFormatting/Podfile (88%) rename Samples/{ => Skyflow}/InputFormatting/Podfile.lock (74%) rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj (100%) rename Samples/{InputFormatting/InputFormatting.xcodeproj => Skyflow/UpsertFeature/UpsertFeature.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{InputFormatting/InputFormatting.xcworkspace => Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (55%) rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature/AppDelegate.swift (100%) rename Samples/{InputFormatting/InputFormatting => Skyflow/UpsertFeature/UpsertFeature}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Samples/{InputFormatting/InputFormatting => Skyflow/UpsertFeature/UpsertFeature}/Assets.xcassets/Contents.json (100%) rename Samples/{InputFormatting/InputFormatting => Skyflow/UpsertFeature/UpsertFeature}/Base.lproj/LaunchScreen.storyboard (100%) rename Samples/{InputFormatting/InputFormatting => Skyflow/UpsertFeature/UpsertFeature}/Base.lproj/Main.storyboard (100%) rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature/Info.plist (100%) create mode 100644 Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature/SceneDelegate.swift (100%) rename Samples/{ => Skyflow}/UpsertFeature/UpsertFeature/ViewController.swift (72%) rename Samples/{ => Skyflow}/Validations/Podfile (81%) rename Samples/{ => Skyflow}/Validations/README.md (100%) rename Samples/{ => Skyflow}/Validations/Validations.xcodeproj/project.pbxproj (100%) rename Samples/{UpsertFeature/UpsertFeature.xcodeproj => Skyflow/Validations/Validations.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename Samples/{UpsertFeature/UpsertFeature.xcodeproj => Skyflow/Validations/Validations.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Samples/{ => Skyflow}/Validations/Validations/AppDelegate.swift (100%) rename Samples/{UpdateDataUsingElements/UpdateDataUsingElements => Skyflow/Validations/Validations}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Samples/{ => Skyflow}/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Samples/{UpdateDataUsingElements/UpdateDataUsingElements => Skyflow/Validations/Validations}/Assets.xcassets/Contents.json (100%) rename Samples/{UpdateDataUsingElements/UpdateDataUsingElements => Skyflow/Validations/Validations}/Base.lproj/LaunchScreen.storyboard (100%) rename Samples/{UpdateDataUsingElements/UpdateDataUsingElements => Skyflow/Validations/Validations}/Base.lproj/Main.storyboard (100%) rename Samples/{ => Skyflow}/Validations/Validations/ExampleAPICallback.swift (100%) rename Samples/{ => Skyflow}/Validations/Validations/ExampleTokenProvider.swift (100%) rename Samples/{ => Skyflow}/Validations/Validations/Info.plist (100%) rename Samples/{ => Skyflow}/Validations/Validations/SceneDelegate.swift (100%) rename Samples/{ => Skyflow}/Validations/Validations/ViewController.swift (100%) delete mode 100644 Samples/UpdateDataUsingElements/Podfile delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift delete mode 100644 Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift delete mode 100644 Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard delete mode 100644 Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard delete mode 100644 Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift delete mode 100644 Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Samples/Validations/Validations/Assets.xcassets/Contents.json delete mode 100644 Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard delete mode 100644 Samples/Validations/Validations/Base.lproj/Main.storyboard create mode 100644 Skyflow-flowvault-ios.podspec create mode 100644 Sources/Skyflow/Exports.swift rename Sources/Skyflow/{core => }/Init.swift (87%) delete mode 100644 Sources/Skyflow/Resources/Assets.xcassets/Contents.json delete mode 100644 Sources/Skyflow/Version.swift rename Sources/Skyflow/collect/{CollectContainer.swift => CollectContainerOperations.swift} (84%) create mode 100644 Sources/Skyflow/collect/ComposableContainerOperations.swift rename Sources/Skyflow/core/{APIClient.swift => APIClientLegacy.swift} (59%) create mode 100644 Sources/Skyflow/core/ClientOperations.swift create mode 100644 Sources/Skyflow/core/ContainerOptions.swift delete mode 100644 Sources/Skyflow/core/ContextOptions.swift delete mode 100644 Sources/Skyflow/core/container/Container.swift delete mode 100644 Sources/Skyflow/core/container/ContainerOptions.swift create mode 100644 Sources/Skyflow/elements/CollectElementInput.swift create mode 100644 Sources/Skyflow/elements/CollectElementOptions.swift delete mode 100644 Sources/Skyflow/elements/core/CollectElementInput.swift rename Sources/Skyflow/reveal/{RevealContainer.swift => RevealContainerOperations.swift} (79%) delete mode 100644 Sources/Skyflow/skyflow_iOS.swift rename Sources/{Skyflow => SkyflowCore}/BaseElement.swift (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json (100%) rename {Samples/UpsertFeature/UpsertFeature => Sources/SkyflowCore/Resources}/Assets.xcassets/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/checkmark.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/checkmark.imageset/checkmark.png (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/dropdown.imageset/Contents.json (100%) rename Sources/{Skyflow => SkyflowCore}/Resources/Assets.xcassets/dropdown.imageset/dropdown.png (100%) create mode 100644 Sources/SkyflowCore/Version.swift create mode 100644 Sources/SkyflowCore/collect/CollectContainer.swift rename Sources/{Skyflow => SkyflowCore}/collect/TokenAPICallback.swift (100%) rename Sources/{Skyflow => SkyflowCore}/composable/AutoshiftAllowed.swift (100%) rename Sources/{Skyflow => SkyflowCore}/composable/ComposableContainer.swift (62%) create mode 100644 Sources/SkyflowCore/core/APIClient.swift create mode 100644 Sources/SkyflowCore/core/BaseConfiguration.swift create mode 100644 Sources/SkyflowCore/core/ClientBase.swift create mode 100644 Sources/SkyflowCore/core/ContextOptions.swift rename Sources/{Skyflow => SkyflowCore}/core/Element.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/Env.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/EventName.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/InterfaceName.swift (95%) rename Sources/{Skyflow => SkyflowCore}/core/Log.swift (66%) rename Sources/{Skyflow => SkyflowCore}/core/LogLevel.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/Message.swift (99%) rename Sources/{Skyflow => SkyflowCore}/core/Options.swift (100%) create mode 100644 Sources/SkyflowCore/core/container/BaseContainerOptions.swift create mode 100644 Sources/SkyflowCore/core/container/Container.swift rename Sources/{Skyflow => SkyflowCore}/core/container/ContainerProtocol.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/container/ContainerType.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/protocols/Callback.swift (100%) rename Sources/{Skyflow => SkyflowCore}/core/protocols/TokenProvider.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/FormatTextField.swift (97%) rename Sources/{Skyflow => SkyflowCore}/elements/PaddingLabel.swift (78%) rename Sources/{Skyflow => SkyflowCore}/elements/SkyflowElement.swift (80%) rename Sources/{Skyflow => SkyflowCore}/elements/TextField.swift (96%) rename Sources/{Skyflow => SkyflowCore}/elements/TextFieldValidationDelegate.swift (100%) create mode 100644 Sources/SkyflowCore/elements/core/BaseCollectElementInput.swift rename Sources/{Skyflow/elements/core/CollectElementOptions.swift => SkyflowCore/elements/core/BaseCollectElementOptions.swift} (54%) rename Sources/{Skyflow => SkyflowCore}/elements/core/ElementType.swift (96%) rename Sources/{Skyflow => SkyflowCore}/elements/core/state/State.swift (91%) rename Sources/{Skyflow => SkyflowCore}/elements/core/state/StateforText.swift (98%) rename Sources/{Skyflow => SkyflowCore}/elements/core/styles/Style.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/core/styles/Styles.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/utils/CardBin.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/utils/CardType.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/utils/CharacterSet.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/ElementValueMatchRule.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/LengthMatchRule.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/RegexMatchRule.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowInternalValidatorProtocol.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidateCardExpirationDate.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidateCardNumber.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidateExpirationMonth.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidateExpirationYear.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidateLengthMatch.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidationError.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/SkyflowValidator.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/ValidationRule.swift (100%) rename Sources/{Skyflow => SkyflowCore}/elements/validations/ValidationSet.swift (91%) rename Sources/{Skyflow => SkyflowCore}/errors/ErrorCodes.swift (57%) create mode 100644 Sources/SkyflowCore/reveal/BaseRevealElementInput.swift create mode 100644 Sources/SkyflowCore/reveal/BaseRevealElementOptions.swift rename Sources/{Skyflow => SkyflowCore}/reveal/ConversionHelpers.swift (79%) rename Sources/{Skyflow => SkyflowCore}/reveal/FormatLabel.swift (100%) rename Sources/{Skyflow => SkyflowCore}/reveal/Label.swift (93%) rename Sources/{Skyflow => SkyflowCore}/reveal/RedactionType.swift (100%) create mode 100644 Sources/SkyflowCore/reveal/RevealContainer.swift rename Sources/{Skyflow => SkyflowCore}/reveal/RevealErrorRecord.swift (54%) rename Sources/{Skyflow => SkyflowCore}/reveal/RevealSuccessRecord.swift (54%) rename Sources/{Skyflow => SkyflowCore}/reveal/SkyflowLabelView.swift (96%) rename Sources/{Skyflow => SkyflowCore}/reveal/UILabel+Copyable.swift (97%) rename Sources/{Skyflow => SkyflowCore}/utils/DictionaryExtension.swift (96%) rename Sources/{Skyflow => SkyflowCore}/utils/FetchMetrices.swift (81%) create mode 100644 Sources/SkyflowCore/utils/SkyflowAssetBundle.swift rename Sources/{Skyflow => SkyflowCore}/utils/StringExtension.swift (98%) create mode 100644 Sources/SkyflowFlowVaultIOS/Exports.swift create mode 100644 Sources/SkyflowFlowVaultIOS/Init.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/AdditionalFields.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/CVVTokenReplacer.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/CollectOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/CollectResponse.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultICOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/InsertOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/collect/UpsertOption.swift create mode 100644 Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift create mode 100644 Sources/SkyflowFlowVaultIOS/core/Client.swift create mode 100644 Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift create mode 100644 Sources/SkyflowFlowVaultIOS/core/Configuration.swift create mode 100644 Sources/SkyflowFlowVaultIOS/core/ContainerOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/elements/CollectElementInput.swift create mode 100644 Sources/SkyflowFlowVaultIOS/elements/CollectElementOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealElementInput.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealElementOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealOptions.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealRequestRecord.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealResponse.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealValueCallback.swift create mode 100644 Sources/SkyflowFlowVaultIOS/reveal/TokenGroupRedaction.swift create mode 100644 Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift create mode 100644 Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift rename Tests/{skyflow-iOS-utilTests => skyflow-iOS-getByIdTests}/skyflow_iOS_getByIdUtilTests.swift (99%) rename Tests/{skyflow-iOS-utilTests => skyflow-iOS-getTests}/skyflow_iOS_getUtilTests.swift (99%) create mode 100644 Tests/skyflow-iOS-legacyTests/DemoImpl.swift create mode 100644 Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift create mode 100644 Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift create mode 100644 Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift create mode 100644 Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift create mode 100644 Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift create mode 100644 Tests/skyflow-iOS-utilTests/MockURLProtocol.swift create mode 100644 Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index ef954b81..cbb4527e 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -24,7 +24,9 @@ jobs: - name: Deploy to Cocoapods run: | set -eo pipefail - pod lib lint --allow-warnings - pod trunk push --allow-warnings + pod lib lint Skyflow.podspec --allow-warnings + pod lib lint Skyflow-flowvault-ios.podspec --allow-warnings + pod trunk push Skyflow.podspec --allow-warnings + pod trunk push Skyflow-flowvault-ios.podspec --allow-warnings env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 056f9f4d..d56507d8 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -8,7 +8,7 @@ on: paths-ignore: - 'Skyflow.podspec' - '*.md' - - 'Sources/Skyflow/Version.swift' + - 'Sources/SkyflowCore/Version.swift' branches: - public-release/* @@ -41,7 +41,7 @@ jobs: run: | git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com - git add Skyflow.podspec - git add Sources/Skyflow/Version.swift + git add Skyflow.podspec Skyflow-flowvault-ios.podspec + git add Sources/SkyflowCore/Version.swift git commit -m "[AUTOMATED] Public Release ${{ steps.version.outputs.version }}" git push origin \ No newline at end of file diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index 9b0d490d..97678fe0 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -7,8 +7,9 @@ on: - '*' paths-ignore: - 'Skyflow.podspec' + - 'Skyflow-flowvault-ios.podspec' - '*.md' - - 'Sources/Skyflow/Version.swift' + - 'Sources/SkyflowCore/Version.swift' branches: - release/* @@ -36,8 +37,8 @@ jobs: run: | git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com - git add Skyflow.podspec - git add Sources/Skyflow/Version.swift + git add Skyflow.podspec Skyflow-flowvault-ios.podspec + git add Sources/SkyflowCore/Version.swift git commit -m "[AUTOMATED] Private Release ${{ steps.previoustag.outputs.tag }}.$(git rev-parse --short $GITHUB_SHA)" git push origin @@ -53,4 +54,5 @@ jobs: run: | pod repo add private_spec git@github.com:skyflowapi/skyflow-iOS-private-spec.git pod repo push private_spec ./Skyflow.podspec --allow-warnings + pod repo push private_spec ./Skyflow-flowvault-ios.podspec --allow-warnings echo "Published as Private pod" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2be397f1..fd5698f8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -44,7 +44,7 @@ jobs: xcrun simctl list devices booted - name: Build and Test - run: xcodebuild clean -workspace .swiftpm/xcode/package.xcworkspace -scheme Skyflow -enableCodeCoverage YES -destination "platform=iOS Simulator,OS=18.6,name=iPhone 16" VAULT_URL="${{ secrets.VAULT_URL }}" TEST_SKYFLOW_ID1="${{ secrets.TEST_SKYFLOW_ID1 }}" TEST_SKYFLOW_ID2="${{ secrets.TEST_SKYFLOW_ID2 }}" TEST_SKYFLOW_ID3="${{ secrets.TEST_SKYFLOW_ID3 }}" TEST_SKYFLOW_ID4="${{ secrets.TEST_SKYFLOW_ID4 }}" VAULT_ID="${{ secrets.VAULT_ID }}" test + run: xcodebuild clean -workspace .swiftpm/xcode/package.xcworkspace -scheme Skyflow-Package -enableCodeCoverage YES -destination "platform=iOS Simulator,OS=18.6,name=iPhone 16" VAULT_URL="${{ secrets.VAULT_URL }}" TEST_SKYFLOW_ID1="${{ secrets.TEST_SKYFLOW_ID1 }}" TEST_SKYFLOW_ID2="${{ secrets.TEST_SKYFLOW_ID2 }}" TEST_SKYFLOW_ID3="${{ secrets.TEST_SKYFLOW_ID3 }}" TEST_SKYFLOW_ID4="${{ secrets.TEST_SKYFLOW_ID4 }}" VAULT_ID="${{ secrets.VAULT_ID }}" test - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a478f038..5855abf6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,9 @@ jobs: - name: Deploy to Cocoapods run: | set -eo pipefail - pod lib lint --allow-warnings - pod trunk push --allow-warnings + pod lib lint Skyflow.podspec --allow-warnings + pod lib lint Skyflow-flowvault-ios.podspec --allow-warnings + pod trunk push Skyflow.podspec --allow-warnings + pod trunk push Skyflow-flowvault-ios.podspec --allow-warnings env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/Package.swift b/Package.swift index b77ae820..8dbbe82b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,47 +1,66 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Skyflow", + platforms: [ + .iOS(.v13) + ], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. + // Legacy vault SDK 1(v1 API contract). .library( name: "Skyflow", targets: ["Skyflow"]), + // FlowVault SDK (v2 API contract). Module name is SkyflowFlowVaultIOS. + .library( + name: "Skyflow-flowvault-ios", + targets: ["SkyflowFlowVaultIOS"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. + // Shared, contract-agnostic core: UI elements, validations, styles, + // container machinery, token/JWT handling, logging, errors, utils. .target( - name: "Skyflow", + name: "SkyflowCore", dependencies: [], resources: [ .process("Resources") ] ), + // Legacy (v1) contract layer. + .target( + name: "Skyflow", + dependencies: ["SkyflowCore"] + ), + // FlowVault (v2) contract layer. + .target( + name: "SkyflowFlowVaultIOS", + dependencies: ["SkyflowCore"] + ), .testTarget( name: "skyflow-iOS-collectTests", - dependencies: ["Skyflow"]), + dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-revealTests", - dependencies: ["Skyflow"]), + dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-errorTests", - dependencies: ["Skyflow"]), + dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-getByIdTests", - dependencies: ["Skyflow"]), + dependencies: ["Skyflow", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-elementTests", - dependencies: ["Skyflow"]), + dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-utilTests", - dependencies: ["Skyflow"]), + dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), + .testTarget(name: "skyflow-iOS-legacyTests", + dependencies: ["Skyflow", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-scenarioTests", - dependencies: ["Skyflow"]), + dependencies: ["Skyflow", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-getTests", - dependencies: ["Skyflow"]), - .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["Skyflow"]) + dependencies: ["Skyflow", "SkyflowCore"]), + .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]) ] ) diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift deleted file mode 100644 index 0d8be466..00000000 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift b/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift deleted file mode 100644 index 0d8be466..00000000 --- a/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/ComposableElements/ComposableElements/ResponseStructs.swift b/Samples/ComposableElements/ComposableElements/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/Samples/ComposableElements/ComposableElements/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/Samples/GetSample/GetSample.xcodeproj/project.pbxproj b/Samples/GetSample/GetSample.xcodeproj/project.pbxproj deleted file mode 100644 index bdef97b9..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.pbxproj +++ /dev/null @@ -1,640 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */; }; - 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */; }; - 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */; }; - 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */; }; - 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */; }; - 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */; }; - 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */; }; - 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */; }; - 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */; }; - 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */; }; - 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */; }; - 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */; }; - 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 7C7BCD532A49AD6400C6FBCF /* Skyflow */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; - remoteInfo = GetSample; - }; - 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; - remoteInfo = GetSample; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 7C7BCD192A49A5B900C6FBCF /* GetSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GetSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 7C7BCD232A49A5B900C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 7C7BCD282A49A5BA00C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleTests.swift; sourceTree = ""; }; - 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITests.swift; sourceTree = ""; }; - 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITestsLaunchTests.swift; sourceTree = ""; }; - 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; - 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; - 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 7C7BCD162A49A5B900C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD362A49A5BB00C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 7C7BCD102A49A5B900C6FBCF = { - isa = PBXGroup; - children = ( - 7C7BCD1B2A49A5B900C6FBCF /* GetSample */, - 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */, - 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */, - 7C7BCD1A2A49A5B900C6FBCF /* Products */, - ); - sourceTree = ""; - }; - 7C7BCD1A2A49A5B900C6FBCF /* Products */ = { - isa = PBXGroup; - children = ( - 7C7BCD192A49A5B900C6FBCF /* GetSample.app */, - 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */, - 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 7C7BCD1B2A49A5B900C6FBCF /* GetSample */ = { - isa = PBXGroup; - children = ( - 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */, - 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */, - 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */, - 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */, - 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */, - 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */, - 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */, - 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */, - 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */, - 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */, - ); - path = GetSample; - sourceTree = ""; - }; - 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */ = { - isa = PBXGroup; - children = ( - 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */, - ); - path = GetSampleTests; - sourceTree = ""; - }; - 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */ = { - isa = PBXGroup; - children = ( - 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */, - 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */, - ); - path = GetSampleUITests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7C7BCD182A49A5B900C6FBCF /* GetSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */; - buildPhases = ( - 7C7BCD152A49A5B900C6FBCF /* Sources */, - 7C7BCD162A49A5B900C6FBCF /* Frameworks */, - 7C7BCD172A49A5B900C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GetSample; - packageProductDependencies = ( - 7C7BCD532A49AD6400C6FBCF /* Skyflow */, - ); - productName = GetSample; - productReference = 7C7BCD192A49A5B900C6FBCF /* GetSample.app */; - productType = "com.apple.product-type.application"; - }; - 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */; - buildPhases = ( - 7C7BCD2B2A49A5BB00C6FBCF /* Sources */, - 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */, - 7C7BCD2D2A49A5BB00C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */, - ); - name = GetSampleTests; - productName = GetSampleTests; - productReference = 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */; - buildPhases = ( - 7C7BCD352A49A5BB00C6FBCF /* Sources */, - 7C7BCD362A49A5BB00C6FBCF /* Frameworks */, - 7C7BCD372A49A5BB00C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */, - ); - name = GetSampleUITests; - productName = GetSampleUITests; - productReference = 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7C7BCD112A49A5B900C6FBCF /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; - TargetAttributes = { - 7C7BCD182A49A5B900C6FBCF = { - CreatedOnToolsVersion = 14.0; - }; - 7C7BCD2E2A49A5BB00C6FBCF = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 7C7BCD182A49A5B900C6FBCF; - }; - 7C7BCD382A49A5BB00C6FBCF = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 7C7BCD182A49A5B900C6FBCF; - }; - }; - }; - buildConfigurationList = 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7C7BCD102A49A5B900C6FBCF; - packageReferences = ( - 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */, - ); - productRefGroup = 7C7BCD1A2A49A5B900C6FBCF /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7C7BCD182A49A5B900C6FBCF /* GetSample */, - 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */, - 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7C7BCD172A49A5B900C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */, - 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */, - 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2D2A49A5BB00C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD372A49A5BB00C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7C7BCD152A49A5B900C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */, - 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */, - 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */, - 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */, - 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */, - 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2B2A49A5BB00C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD352A49A5BB00C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */, - 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; - targetProxy = 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */; - }; - 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; - targetProxy = 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7C7BCD232A49A5B900C6FBCF /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7C7BCD282A49A5BA00C6FBCF /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 7C7BCD412A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7C7BCD422A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7C7BCD442A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GetSample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7C7BCD452A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GetSample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 7C7BCD472A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; - }; - name = Debug; - }; - 7C7BCD482A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; - }; - name = Release; - }; - 7C7BCD4A2A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = GetSample; - }; - name = Debug; - }; - 7C7BCD4B2A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = GetSample; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD412A49A5BB00C6FBCF /* Debug */, - 7C7BCD422A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD442A49A5BB00C6FBCF /* Debug */, - 7C7BCD452A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD472A49A5BB00C6FBCF /* Debug */, - 7C7BCD482A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD4A2A49A5BB00C6FBCF /* Debug */, - 7C7BCD4B2A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 7C7BCD532A49AD6400C6FBCF /* Skyflow */ = { - isa = XCSwiftPackageProductDependency; - package = 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */; - productName = Skyflow; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 7C7BCD112A49A5B900C6FBCF /* Project object */; -} diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index f5762feb..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "aexml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tadija/AEXML.git", - "state" : { - "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version" : "4.6.1" - } - }, - { - "identity" : "skyflow-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/skyflowapi/skyflow-iOS.git", - "state" : { - "branch" : "main", - "revision" : "d746cff983d44dd4690780c14aa7ca60387d7850" - } - } - ], - "version" : 2 -} diff --git a/Samples/GetSample/GetSample/AppDelegate.swift b/Samples/GetSample/GetSample/AppDelegate.swift deleted file mode 100644 index aec625c4..00000000 --- a/Samples/GetSample/GetSample/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/Samples/GetSample/GetSample/ExampleAPICallback.swift b/Samples/GetSample/GetSample/ExampleAPICallback.swift deleted file mode 100644 index 530d74fd..00000000 --- a/Samples/GetSample/GetSample/ExampleAPICallback.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ExampleAPICallback.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/GetSample/GetSample/ExampleTokenProvider.swift b/Samples/GetSample/GetSample/ExampleTokenProvider.swift deleted file mode 100644 index 75a0efee..00000000 --- a/Samples/GetSample/GetSample/ExampleTokenProvider.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleTokenProvider: TokenProvider { - public func getBearerToken(_ apiCallback: Skyflow.Callback) { - if let url = URL(string: "") { - let session = URLSession(configuration: .default) - let task = session.dataTask(with: url) { data, _, error in - if error != nil { - print(error!) - return - } - if let safeData = data { - do { - let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] - if let accessToken = x?["accessToken"] { - apiCallback.onSuccess(accessToken) - } - } catch { - print("access token wrong format") - } - } - } - task.resume() - } - } -} diff --git a/Samples/GetSample/GetSample/ResponseStructs.swift b/Samples/GetSample/GetSample/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/Samples/GetSample/GetSample/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/Samples/GetSample/GetSample/SceneDelegate.swift b/Samples/GetSample/GetSample/SceneDelegate.swift deleted file mode 100644 index a968b5bc..00000000 --- a/Samples/GetSample/GetSample/SceneDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SceneDelegate.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/Samples/GetSample/GetSample/ViewController.swift b/Samples/GetSample/GetSample/ViewController.swift deleted file mode 100644 index df14f26a..00000000 --- a/Samples/GetSample/GetSample/ViewController.swift +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import UIKit -import Skyflow - -class ViewController: UIViewController { - private var skyflow: Skyflow.Client? - private var stackView: UIStackView! - - override func loadView() { - let view = UIView() - view.backgroundColor = .white - view.translatesAutoresizingMaskIntoConstraints = false - self.view = view - } - - override func viewDidLoad() { - super.viewDidLoad() - - let tokenProvider = ExampleTokenProvider() - self.stackView = UIStackView() - - let config = Skyflow.Configuration( - vaultID: "", - vaultURL: "", - tokenProvider: tokenProvider, - options: Skyflow.Options( - logLevel: Skyflow.LogLevel.DEBUG - ) - ) - - self.skyflow = Skyflow.initialize(config) - - if self.skyflow != nil { - let getWithRedactionType = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) - getWithRedactionType.backgroundColor = .gray - getWithRedactionType.setTitle("Get Records With Redaction Type", for: .normal) - getWithRedactionType.addTarget(self, action: #selector(getRecordsWithRedactionType), for: .touchUpInside) - - let getWithGetOptionTokens = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) - getWithGetOptionTokens.backgroundColor = .gray - getWithGetOptionTokens.setTitle("Get Records with tokens", for: .normal) - getWithGetOptionTokens.addTarget(self, action: #selector(getRecordsWithGetOptionTokens), for: .touchUpInside) - - stackView.addArrangedSubview(getWithRedactionType) - stackView.addArrangedSubview(getWithGetOptionTokens) - - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 10 - stackView.alignment = .fill - stackView.translatesAutoresizingMaskIntoConstraints = false - let scrollView = UIScrollView(frame: .zero) - scrollView.isScrollEnabled = true - scrollView.backgroundColor = .white - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - scrollView.topAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: 20 - ).isActive = true - scrollView.leftAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.leftAnchor, - constant: 10 - ).isActive = true - scrollView.rightAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.rightAnchor, - constant: -10 - ).isActive = true - scrollView.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor - ).isActive = true - scrollView.addSubview(stackView) - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true - stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true - stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - } - } - @objc func getRecordsWithRedactionType() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let record = ["ids": ["", ""], "table": "", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - let recordColumn = ["columnValues": ["", ""],"table": "", "columnName": "","redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, recordColumn, badRecord]] - self.skyflow?.get(records: records, callback: exampleAPICallback) - } - - @objc func getRecordsWithGetOptionTokens() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let record = ["ids": ["", ""], "table": ""] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, badRecord]] - self.skyflow?.get(records: records, options: GetOptions(tokens: true), callback: exampleAPICallback) - } - internal func updateSuccess(_ response: SuccessResponse) { - print(response) - print("Successfully got response:", response) - } - internal func updateFailure(error: Any) { - print("Failed Operation", error) - } -} diff --git a/Samples/GetSample/GetSampleTests/GetSampleTests.swift b/Samples/GetSample/GetSampleTests/GetSampleTests.swift deleted file mode 100644 index 4837e722..00000000 --- a/Samples/GetSample/GetSampleTests/GetSampleTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// GetSampleTests.swift -// GetSampleTests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest -@testable import GetSample - -final class GetSampleTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift b/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift deleted file mode 100644 index 8b26e9ab..00000000 --- a/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// GetSampleUITests.swift -// GetSampleUITests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest - -final class GetSampleUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift b/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift deleted file mode 100644 index 75f3db9b..00000000 --- a/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// GetSampleUITestsLaunchTests.swift -// GetSampleUITests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest - -final class GetSampleUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/Samples/GetSample/Podfile b/Samples/GetSample/Podfile deleted file mode 100644 index 72b31da4..00000000 --- a/Samples/GetSample/Podfile +++ /dev/null @@ -1,12 +0,0 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -source 'https://github.com/CocoaPods/Specs.git' - -target 'GetSample' do - #use_frameworks! - pod 'AEXML', '4.6.1' - # Pods for GetSample - pod 'Skyflow', ">= 1.22.0" - -end diff --git a/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3e..00000000 --- a/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift b/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift deleted file mode 100644 index 25f84e24..00000000 --- a/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ -// Created by Bharti Sagar on 26/04/23. -// - -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/InputFormatting/InputFormatting/Info.plist b/Samples/InputFormatting/InputFormatting/Info.plist deleted file mode 100644 index dd3c9afd..00000000 --- a/Samples/InputFormatting/InputFormatting/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/Samples/InputFormatting/InputFormatting/ResponseStructs.swift b/Samples/InputFormatting/InputFormatting/ResponseStructs.swift deleted file mode 100644 index 2230b92f..00000000 --- a/Samples/InputFormatting/InputFormatting/ResponseStructs.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// Created by Bharti Sagar on 26/04/23. -// - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let ssn: String - let phone_number: String - let license_number: String - let skyflow_id: String -} diff --git a/Samples/README.md b/Samples/README.md index a8d81063..2b4266ec 100644 --- a/Samples/README.md +++ b/Samples/README.md @@ -1,13 +1,37 @@ -Please follow these below steps to run samples +# Samples -- Navigate to the desired sample +Sample apps are organized by SDK — pick the folder for the product you use. + +## [`Skyflow/`](Skyflow/) — legacy Skyflow SDK (`pod 'Skyflow'` / `import Skyflow`) + +| Sample | Demonstrates | +|---|---| +| [CollectAndRevealSample](Skyflow/CollectAndRevealSample/) | Collecting card data with secure elements and revealing tokens (incl. card brand choice) | +| [ComposableElements](Skyflow/ComposableElements/) | Multiple fields composed into one container/row | +| [InputFormatting](Skyflow/InputFormatting/) | Format/translation patterns on collect elements | +| [UpsertFeature](Skyflow/UpsertFeature/) | Insert with upsert options | +| [Validations](Skyflow/Validations/) | Built-in and custom validation rules | + +## [`SkyflowFlowVaultIOS/`](SkyflowFlowVaultIOS/) — Skyflow FlowVault SDK (`pod 'Skyflow-flowvault-ios'` / `import SkyflowFlowVaultIOS`) + +| Sample | Demonstrates | +|---|---| +| [UpdateDataUsingElements](SkyflowFlowVaultIOS/UpdateDataUsingElements/) | Updating vault records via collect elements with `skyflowId`; its Podfile also shows installing **both** SDKs side by side in one app | + +## Running a sample + +- Navigate to the desired sample (`Samples//`) - In CMD, run the command, pod install - Open the .xcworkspace file using xcode - Click on Build and run +Most Podfiles reference the published pods. `UpdateDataUsingElements` uses +`:path => '../../../'` to build the SDKs from this repository checkout — +useful for testing local changes. + `Note`: -In every sample, in Skyflow.Config(), replace with the following fields: +In every sample, in the Skyflow configuration, replace the following fields: 1. Replace the placeholder "" with the correct vaultId you want to connect 2. Replace the placeholder "" with the correct vaultURL 3. Implement the bearer token endpoint using server side auth SDK and service account file. - Replace the placeholder "" with the bearer token endpoint which gives the bearerToken, implemented at your backend. \ No newline at end of file + Replace the placeholder "" with the bearer token endpoint which gives the bearerToken, implemented at your backend. diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..31459b56 --- /dev/null +++ b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "9d7a641543031e54b3470012de29bf3cd9a042920b5fe71a347a8d0e07ccefe2", + "pins" : [ + { + "identity" : "skyflow-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/skyflowapi/skyflow-iOS.git", + "state" : { + "revision" : "cb30c6b6b8a36105fa4bf8ba6a879af8b777d2c7", + "version" : "1.25.1" + } + } + ], + "version" : 3 +} diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift similarity index 97% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift index 270ff189..9c7c742b 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift +++ b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift @@ -39,7 +39,7 @@ class CardBrandChoiceSampleViewController: UIViewController { let iconStyles = Skyflow.Styles(base: Style(cardIconAlignment: .right)) // create card number element - let collectCardNumberInput = Skyflow.CollectElementInput(table: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: Skyflow.ElementType.CARD_NUMBER) + let collectCardNumberInput = Skyflow.CollectElementInput(tableName: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: Skyflow.ElementType.CARD_NUMBER) let requiredOption = Skyflow.CollectElementOptions(required: true, enableCardIcon: true, enableCopy: false) collectCardNumberElement = container?.create(input: collectCardNumberInput, options: requiredOption) diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift similarity index 68% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 48368c2d..9f334fac 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -22,6 +22,12 @@ class CollectAndRevealViewController: UIViewController { private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] + + // Manual/visual check for the mock-CVV feature: shows the CVV token straight from + // collect()'s response so you can see it's a short numeric mock (not a real vault token) + // after submitting, without needing to inspect console output. + private var cvvMockCheckLabel: UILabel? override func viewDidLoad() { @@ -67,7 +73,7 @@ class CollectAndRevealViewController: UIViewController { ) let collectCardNumberInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", @@ -75,7 +81,7 @@ class CollectAndRevealViewController: UIViewController { type: Skyflow.ElementType.CARD_NUMBER ) let collectNameInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -83,7 +89,7 @@ class CollectAndRevealViewController: UIViewController { type: Skyflow.ElementType.CARDHOLDER_NAME ) let collectCVVInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", @@ -91,7 +97,7 @@ class CollectAndRevealViewController: UIViewController { type: .CVV ) let collectExpMonthInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -99,7 +105,7 @@ class CollectAndRevealViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -116,6 +122,14 @@ class CollectAndRevealViewController: UIViewController { collectButton.backgroundColor = .blue collectButton.setTitle("Submit", for: .normal) collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + + let cvvMockCheckLabel = UILabel() + cvvMockCheckLabel.text = "CVV token will appear here after submit" + cvvMockCheckLabel.numberOfLines = 0 + cvvMockCheckLabel.textColor = .darkGray + cvvMockCheckLabel.font = .systemFont(ofSize: 13) + self.cvvMockCheckLabel = cvvMockCheckLabel + self.revealContainer = skyflow?.container(type: Skyflow.ContainerType.REVEAL, options: nil) self.revealButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) revealButton.backgroundColor = .blue @@ -127,23 +141,23 @@ class CollectAndRevealViewController: UIViewController { stackView.addArrangedSubview(collectExpMonth!) stackView.addArrangedSubview(collectExpYear!) stackView.addArrangedSubview(collectButton) - + stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 5 stackView.alignment = .fill stackView.translatesAutoresizingMaskIntoConstraints = false - + let scrollView = UIScrollView(frame: .zero) scrollView.isScrollEnabled = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) - + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2).isActive = true scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 2).isActive = true scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - + scrollView.addSubview(stackView) stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true @@ -153,26 +167,51 @@ class CollectAndRevealViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } + ) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let tokens = response.records.first?.tokens { + updateRevealInputs(tokens: tokens) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Skyflow.SkyflowError) { + if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error) + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") + } + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -188,40 +227,48 @@ class CollectAndRevealViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("REDACTED", forColumn: "card_number") + addTokenGroupRedaction("MASKED", forColumn: "cvv") + addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") + addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") + let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: self.firstToken(tokens, column: "card_number").token, inputStyles: revealStyles, - label: "Card Number", - redaction: .REDACTED + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: self.firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, - label: "CVV", - redaction: .MASKED + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: self.firstToken(tokens, column: "cardholder_name").token, inputStyles: revealStyles, - label: "Card Holder Name", - redaction: .DEFAULT - + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: self.firstToken(tokens, column: "expiry_month").token, inputStyles: revealStyles, - label: "Expiration Month", - redaction: .PLAIN_TEXT + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: self.firstToken(tokens, column: "expiry_year").token, inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift new file mode 100644 index 00000000..12e3bd84 --- /dev/null +++ b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see CollectAndRevealViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Info.plist similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Info.plist diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift new file mode 100644 index 00000000..67c28811 --- /dev/null +++ b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift similarity index 100% rename from Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift rename to Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift diff --git a/Samples/CollectAndRevealSample/Podfile b/Samples/Skyflow/CollectAndRevealSample/Podfile similarity index 81% rename from Samples/CollectAndRevealSample/Podfile rename to Samples/Skyflow/CollectAndRevealSample/Podfile index a02fa6bd..ddb60497 100644 --- a/Samples/CollectAndRevealSample/Podfile +++ b/Samples/Skyflow/CollectAndRevealSample/Podfile @@ -5,9 +5,8 @@ source 'https://github.com/CocoaPods/Specs.git' target 'CollectAndRevealSample' do #use_frameworks! - pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.23.0" + pod 'Skyflow', ">= 1.25.0" end diff --git a/Samples/CollectAndRevealSample/README.md b/Samples/Skyflow/CollectAndRevealSample/README.md similarity index 100% rename from Samples/CollectAndRevealSample/README.md rename to Samples/Skyflow/CollectAndRevealSample/README.md diff --git a/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj b/Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.pbxproj similarity index 100% rename from Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj rename to Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.pbxproj diff --git a/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/ComposableElements/ComposableElements/AppDelegate.swift b/Samples/Skyflow/ComposableElements/ComposableElements/AppDelegate.swift similarity index 100% rename from Samples/ComposableElements/ComposableElements/AppDelegate.swift rename to Samples/Skyflow/ComposableElements/ComposableElements/AppDelegate.swift diff --git a/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json rename to Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json b/Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/Contents.json similarity index 100% rename from Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json rename to Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/Contents.json diff --git a/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard b/Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard rename to Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard b/Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/Main.storyboard similarity index 100% rename from Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard rename to Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/Main.storyboard diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift b/Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift new file mode 100644 index 00000000..548a8e4c --- /dev/null +++ b/Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift b/Samples/Skyflow/ComposableElements/ComposableElements/ExampleTokenProvider.swift similarity index 100% rename from Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift rename to Samples/Skyflow/ComposableElements/ComposableElements/ExampleTokenProvider.swift diff --git a/Samples/ComposableElements/ComposableElements/Info.plist b/Samples/Skyflow/ComposableElements/ComposableElements/Info.plist similarity index 100% rename from Samples/ComposableElements/ComposableElements/Info.plist rename to Samples/Skyflow/ComposableElements/ComposableElements/Info.plist diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift b/Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift new file mode 100644 index 00000000..67c28811 --- /dev/null +++ b/Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/ComposableElements/ComposableElements/SceneDelegate.swift b/Samples/Skyflow/ComposableElements/ComposableElements/SceneDelegate.swift similarity index 100% rename from Samples/ComposableElements/ComposableElements/SceneDelegate.swift rename to Samples/Skyflow/ComposableElements/ComposableElements/SceneDelegate.swift diff --git a/Samples/ComposableElements/ComposableElements/Untitled.swift b/Samples/Skyflow/ComposableElements/ComposableElements/Untitled.swift similarity index 100% rename from Samples/ComposableElements/ComposableElements/Untitled.swift rename to Samples/Skyflow/ComposableElements/ComposableElements/Untitled.swift diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/Skyflow/ComposableElements/ComposableElements/ViewController.swift similarity index 68% rename from Samples/ComposableElements/ComposableElements/ViewController.swift rename to Samples/Skyflow/ComposableElements/ComposableElements/ViewController.swift index 5597e404..78404afe 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/Skyflow/ComposableElements/ComposableElements/ViewController.swift @@ -22,6 +22,7 @@ class ViewController: UIViewController { private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -73,7 +74,7 @@ class ViewController: UIViewController { ) let collectCardNumberInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", @@ -81,7 +82,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARD_NUMBER ) let collectNameInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -89,7 +90,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARDHOLDER_NAME ) let collectCVVInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", @@ -97,7 +98,7 @@ class ViewController: UIViewController { type: .CVV ) let collectExpMonthInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -105,7 +106,7 @@ class ViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = Skyflow.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -170,26 +171,65 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } + ) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + + // Upsert example: insert-or-update credit_cards records, matching on card_number. + // If a record with a matching card_number already exists, its fields are merged + // in (updateType: .UPDATE) instead of creating a duplicate row. + let upsertOptions = [ + Skyflow.UpsertOption(tableName: "credit_cards", uniqueColumns: ["card_number"], updateType: .UPDATE) + ] + + // additionalFields: extra records submitted alongside whatever's collected from + // the mounted elements, one entry per table. + let additionalFields = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(tableName: "credit_cards", data: ["billing_zip": "94105"]) + ]) + + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(additionalFields: additionalFields, upsert: upsertOptions)) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let tokens = response.records.first?.tokens { + updateRevealInputs(tokens: tokens) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Skyflow.SkyflowError) { + if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error) + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") + } + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -205,40 +245,48 @@ class ViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("REDACTED", forColumn: "card_number") + addTokenGroupRedaction("MASKED", forColumn: "cvv") + addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") + addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") + let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: self.firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, - label: "Card Number", - redaction: .REDACTED + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: self.firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, - label: "CVV", - redaction: .MASKED + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: self.firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, - label: "Card Holder Name", - redaction: .DEFAULT - + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: self.firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, - label: "Expiration Month", - redaction: .PLAIN_TEXT + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: self.firstToken(tokens, column: "expiry_year").token ?? "", inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift b/Samples/Skyflow/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift similarity index 100% rename from Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift rename to Samples/Skyflow/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift diff --git a/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift b/Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift similarity index 100% rename from Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift rename to Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift diff --git a/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift b/Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift similarity index 100% rename from Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift rename to Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift diff --git a/Samples/ComposableElements/Podfile b/Samples/Skyflow/ComposableElements/Podfile similarity index 88% rename from Samples/ComposableElements/Podfile rename to Samples/Skyflow/ComposableElements/Podfile index 9d8d3f0e..77ef17f8 100644 --- a/Samples/ComposableElements/Podfile +++ b/Samples/Skyflow/ComposableElements/Podfile @@ -4,8 +4,7 @@ target 'ComposableElements' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" # Pods for ComposableElements target 'ComposableElementsTests' do diff --git a/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj b/Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.pbxproj similarity index 100% rename from Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj rename to Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.pbxproj diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata diff --git a/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/InputFormatting/InputFormatting/AppDelegate.swift b/Samples/Skyflow/InputFormatting/InputFormatting/AppDelegate.swift similarity index 100% rename from Samples/InputFormatting/InputFormatting/AppDelegate.swift rename to Samples/Skyflow/InputFormatting/InputFormatting/AppDelegate.swift diff --git a/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json rename to Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/GetSample/GetSample/Assets.xcassets/Contents.json b/Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/Contents.json similarity index 100% rename from Samples/GetSample/GetSample/Assets.xcassets/Contents.json rename to Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/Contents.json diff --git a/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard b/Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard rename to Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/GetSample/GetSample/Base.lproj/Main.storyboard b/Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/Main.storyboard similarity index 100% rename from Samples/GetSample/GetSample/Base.lproj/Main.storyboard rename to Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/Main.storyboard diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift b/Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift new file mode 100644 index 00000000..548a8e4c --- /dev/null +++ b/Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift b/Samples/Skyflow/InputFormatting/InputFormatting/ExampleTokenProvider.swift similarity index 100% rename from Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift rename to Samples/Skyflow/InputFormatting/InputFormatting/ExampleTokenProvider.swift diff --git a/Samples/GetSample/GetSample/Info.plist b/Samples/Skyflow/InputFormatting/InputFormatting/Info.plist similarity index 100% rename from Samples/GetSample/GetSample/Info.plist rename to Samples/Skyflow/InputFormatting/InputFormatting/Info.plist diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift b/Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift new file mode 100644 index 00000000..67c28811 --- /dev/null +++ b/Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/InputFormatting/InputFormatting/SceneDelegate.swift b/Samples/Skyflow/InputFormatting/InputFormatting/SceneDelegate.swift similarity index 100% rename from Samples/InputFormatting/InputFormatting/SceneDelegate.swift rename to Samples/Skyflow/InputFormatting/InputFormatting/SceneDelegate.swift diff --git a/Samples/InputFormatting/InputFormatting/ViewController.swift b/Samples/Skyflow/InputFormatting/InputFormatting/ViewController.swift similarity index 86% rename from Samples/InputFormatting/InputFormatting/ViewController.swift rename to Samples/Skyflow/InputFormatting/InputFormatting/ViewController.swift index fbacc971..c1b5df2d 100644 --- a/Samples/InputFormatting/InputFormatting/ViewController.swift +++ b/Samples/Skyflow/InputFormatting/InputFormatting/ViewController.swift @@ -77,7 +77,7 @@ class ViewController: UIViewController { ) let collectCardNumberInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "card_number", inputStyles: styles, label: "Card Number", @@ -85,7 +85,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARD_NUMBER ) let collectNameInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -93,7 +93,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARDHOLDER_NAME ) let collectCVVInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "cvv", inputStyles: styles, label: "CVV", @@ -101,7 +101,7 @@ class ViewController: UIViewController { type: .CVV ) let collectExpMonthInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -109,7 +109,7 @@ class ViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -117,7 +117,7 @@ class ViewController: UIViewController { type: .EXPIRATION_YEAR ) let collectSSNInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "ssn", inputStyles: styles, label: "SSN", @@ -125,7 +125,7 @@ class ViewController: UIViewController { type: .INPUT_FIELD ) let collectPhoneNumberInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "phone_number", inputStyles: styles, label: "Phone Number", @@ -133,7 +133,7 @@ class ViewController: UIViewController { type: .INPUT_FIELD ) let collectLicenseNumberInput = Skyflow.CollectElementInput( - table: "table", + tableName: "table", column: "license_number", inputStyles: styles, label: "License Number", @@ -201,21 +201,37 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print("reveal success:", response) }, + onFailure: { error in print("reveal failure:", error) } + ) + self.revealContainer?.reveal(callback: revealCallback) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let tokens = response.records.first?.tokens { + updateRevealInputs(tokens: tokens) + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + } + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -232,7 +248,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, label: "Card Number" ) @@ -241,43 +257,43 @@ class ViewController: UIViewController { options: Skyflow.RevealElementOptions(format: "XXXX-XXXX-XXXX-XXXX-XXX", translation: ["X": "[0-9]"]) ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: firstToken(tokens, column: "expiry_year").token ?? "", inputStyles: revealStyles, label: "Expiration Year" ) self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) let revealSSNInput = Skyflow.RevealElementInput( - token: tokens.ssn, + token: firstToken(tokens, column: "ssn").token ?? "", inputStyles: revealStyles, label: "SSN" ) self.revealSSN = self.revealContainer?.create(input: revealSSNInput, options: Skyflow.RevealElementOptions(format: "XXX XX XXXX", translation: ["X": "[0-9]"])) let revealPhoneNumberInput = Skyflow.RevealElementInput( - token: tokens.phone_number, + token: firstToken(tokens, column: "phone_number").token ?? "", inputStyles: revealStyles, label: "Phone Number" ) self.revealPhoneNumber = self.revealContainer?.create(input: revealPhoneNumberInput, options: Skyflow.RevealElementOptions(format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) let revealLicenseNumberInput = Skyflow.RevealElementInput( - token: tokens.phone_number, + token: firstToken(tokens, column: "phone_number").token ?? "", inputStyles: revealStyles, label: "License Number" ) diff --git a/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift b/Samples/Skyflow/InputFormatting/InputFormattingTests/InputFormattingTests.swift similarity index 100% rename from Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift rename to Samples/Skyflow/InputFormatting/InputFormattingTests/InputFormattingTests.swift diff --git a/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift b/Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift similarity index 100% rename from Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift rename to Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift diff --git a/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift b/Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift similarity index 100% rename from Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift rename to Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift diff --git a/Samples/InputFormatting/Podfile b/Samples/Skyflow/InputFormatting/Podfile similarity index 88% rename from Samples/InputFormatting/Podfile rename to Samples/Skyflow/InputFormatting/Podfile index f4ddb57c..354055ad 100644 --- a/Samples/InputFormatting/Podfile +++ b/Samples/Skyflow/InputFormatting/Podfile @@ -3,9 +3,8 @@ target 'InputFormatting' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" # Pods for InputFormatting diff --git a/Samples/InputFormatting/Podfile.lock b/Samples/Skyflow/InputFormatting/Podfile.lock similarity index 74% rename from Samples/InputFormatting/Podfile.lock rename to Samples/Skyflow/InputFormatting/Podfile.lock index e9d81271..7592f10a 100644 --- a/Samples/InputFormatting/Podfile.lock +++ b/Samples/Skyflow/InputFormatting/Podfile.lock @@ -1,18 +1,14 @@ PODS: - - AEXML (4.6.1) - Skyflow (1.18.0-beta.1): - - AEXML DEPENDENCIES: - Skyflow (= 1.18.0-beta.1) SPEC REPOS: trunk: - - AEXML - Skyflow SPEC CHECKSUMS: - AEXML: 1e255ecc6597212f97a7454a69ebd3ede64ac1cf Skyflow: d702f8e9beafba05e164a3f74a6f862596f41085 PODFILE CHECKSUM: 4958176a88d5d7c9eb37117625a3937f5da81019 diff --git a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj b/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj similarity index 100% rename from Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj rename to Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj diff --git a/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 55% rename from Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 040de638..d02f09c6 100644 --- a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "AEXML", - "repositoryURL": "https://github.com/tadija/AEXML.git", - "state": { - "branch": null, - "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version": "4.6.1" - } - }, { "package": "Skyflow", "repositoryURL": "https://github.com/skyflowapi/skyflow-iOS.git", diff --git a/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift b/Samples/Skyflow/UpsertFeature/UpsertFeature/AppDelegate.swift similarity index 100% rename from Samples/UpsertFeature/UpsertFeature/AppDelegate.swift rename to Samples/Skyflow/UpsertFeature/UpsertFeature/AppDelegate.swift diff --git a/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json rename to Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json b/Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json similarity index 100% rename from Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json rename to Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json diff --git a/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard b/Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard rename to Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard b/Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard similarity index 100% rename from Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard rename to Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard diff --git a/Samples/UpsertFeature/UpsertFeature/Info.plist b/Samples/Skyflow/UpsertFeature/UpsertFeature/Info.plist similarity index 100% rename from Samples/UpsertFeature/UpsertFeature/Info.plist rename to Samples/Skyflow/UpsertFeature/UpsertFeature/Info.plist diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift b/Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift new file mode 100644 index 00000000..67c28811 --- /dev/null +++ b/Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift b/Samples/Skyflow/UpsertFeature/UpsertFeature/SceneDelegate.swift similarity index 100% rename from Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift rename to Samples/Skyflow/UpsertFeature/UpsertFeature/SceneDelegate.swift diff --git a/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Samples/Skyflow/UpsertFeature/UpsertFeature/ViewController.swift similarity index 72% rename from Samples/UpsertFeature/UpsertFeature/ViewController.swift rename to Samples/Skyflow/UpsertFeature/UpsertFeature/ViewController.swift index 86f38902..c01a0784 100644 --- a/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Samples/Skyflow/UpsertFeature/UpsertFeature/ViewController.swift @@ -15,6 +15,7 @@ class ViewController: UIViewController { private var revealCvv: Label? private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -40,7 +41,7 @@ class ViewController: UIViewController { textAlignment: .left, textColor: .blue ) - let focusStyle = Skyflow.Style(borderColor: .blue) ̰ + let focusStyle = Skyflow.Style(borderColor: .blue) let completedStyle = Skyflow.Style(borderColor: UIColor.green, textColor: UIColor.green) let invalidStyle = Skyflow.Style(borderColor: UIColor.red, textColor: UIColor.red) let styles = Skyflow.Styles( @@ -85,7 +86,7 @@ class ViewController: UIViewController { ) // keep card number as unique column for testing upsert feature let collectCardNumberInput = Skyflow.CollectElementInput( - table: "persons", + tableName: "persons", column: "cardnumber", inputStyles: styles, labelStyles: labelStyles, @@ -97,7 +98,7 @@ class ViewController: UIViewController { let requiredOption = Skyflow.CollectElementOptions(required: true) let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) let collectCvvInput = Skyflow.CollectElementInput( - table: "persons", + tableName: "persons", column: "cvv", inputStyles: styles, label: "Cvv", @@ -150,25 +151,52 @@ class ViewController: UIViewController { } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } + ) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + let upsertOptions = [Skyflow.UpsertOption(tableName: "persons", uniqueColumns: ["cardnumber"], updateType: .UPDATE)] + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(upsert: upsertOptions)) } - internal func updateSuccess(_ response: SuccessResponse) { - updateRevealInputs(tokens: response.records[0].fields) + internal func updateSuccess(_ response: Skyflow.CollectResponse) { + print(response) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let tokens = response.records.first?.tokens { + updateRevealInputs(tokens: tokens) + } print("Successfully got response:", response) } - internal func updateFailure() { - print("Failed Operation") + internal func updateFailure(error: Skyflow.SkyflowError) { + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") + } + + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -184,21 +212,30 @@ class ViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("DEFAULT", forColumn: "cardnumber") + addTokenGroupRedaction("DEFAULT", forColumn: "cvv") + let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.cardnumber, + token: firstToken(tokens, column: "cardnumber").token, inputStyles: revealStyles, - label: "Card Number", - redaction: .DEFAULT + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCvvInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, - label: "Cvv", - redaction: .DEFAULT + label: "Cvv" ) self.revealCvv = self.revealContainer?.create( input: revealCvvInput, diff --git a/Samples/Validations/Podfile b/Samples/Skyflow/Validations/Podfile similarity index 81% rename from Samples/Validations/Podfile rename to Samples/Skyflow/Validations/Podfile index cb757a40..e8fecb94 100644 --- a/Samples/Validations/Podfile +++ b/Samples/Skyflow/Validations/Podfile @@ -5,8 +5,7 @@ source 'https://github.com/CocoaPods/Specs.git' target 'Validations' do #use_frameworks! - pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" end diff --git a/Samples/Validations/README.md b/Samples/Skyflow/Validations/README.md similarity index 100% rename from Samples/Validations/README.md rename to Samples/Skyflow/Validations/README.md diff --git a/Samples/Validations/Validations.xcodeproj/project.pbxproj b/Samples/Skyflow/Validations/Validations.xcodeproj/project.pbxproj similarity index 100% rename from Samples/Validations/Validations.xcodeproj/project.pbxproj rename to Samples/Skyflow/Validations/Validations.xcodeproj/project.pbxproj diff --git a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Validations/Validations/AppDelegate.swift b/Samples/Skyflow/Validations/Validations/AppDelegate.swift similarity index 100% rename from Samples/Validations/Validations/AppDelegate.swift rename to Samples/Skyflow/Validations/Validations/AppDelegate.swift diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/Skyflow/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json rename to Samples/Skyflow/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/Skyflow/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Samples/Skyflow/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json b/Samples/Skyflow/Validations/Validations/Assets.xcassets/Contents.json similarity index 100% rename from Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json rename to Samples/Skyflow/Validations/Validations/Assets.xcassets/Contents.json diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard b/Samples/Skyflow/Validations/Validations/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard rename to Samples/Skyflow/Validations/Validations/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard b/Samples/Skyflow/Validations/Validations/Base.lproj/Main.storyboard similarity index 100% rename from Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard rename to Samples/Skyflow/Validations/Validations/Base.lproj/Main.storyboard diff --git a/Samples/Validations/Validations/ExampleAPICallback.swift b/Samples/Skyflow/Validations/Validations/ExampleAPICallback.swift similarity index 100% rename from Samples/Validations/Validations/ExampleAPICallback.swift rename to Samples/Skyflow/Validations/Validations/ExampleAPICallback.swift diff --git a/Samples/Validations/Validations/ExampleTokenProvider.swift b/Samples/Skyflow/Validations/Validations/ExampleTokenProvider.swift similarity index 100% rename from Samples/Validations/Validations/ExampleTokenProvider.swift rename to Samples/Skyflow/Validations/Validations/ExampleTokenProvider.swift diff --git a/Samples/Validations/Validations/Info.plist b/Samples/Skyflow/Validations/Validations/Info.plist similarity index 100% rename from Samples/Validations/Validations/Info.plist rename to Samples/Skyflow/Validations/Validations/Info.plist diff --git a/Samples/Validations/Validations/SceneDelegate.swift b/Samples/Skyflow/Validations/Validations/SceneDelegate.swift similarity index 100% rename from Samples/Validations/Validations/SceneDelegate.swift rename to Samples/Skyflow/Validations/Validations/SceneDelegate.swift diff --git a/Samples/Validations/Validations/ViewController.swift b/Samples/Skyflow/Validations/Validations/ViewController.swift similarity index 100% rename from Samples/Validations/Validations/ViewController.swift rename to Samples/Skyflow/Validations/Validations/ViewController.swift diff --git a/Samples/UpdateDataUsingElements/Podfile b/Samples/UpdateDataUsingElements/Podfile deleted file mode 100644 index 288940eb..00000000 --- a/Samples/UpdateDataUsingElements/Podfile +++ /dev/null @@ -1,26 +0,0 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -target 'UpdateDataUsingElements' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - - # Pods for UpdateDataUsingElements - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' - - target 'UpdateDataUsingElementsTests' do - inherit! :search_paths - # Pods for testing - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' - end - - target 'UpdateDataUsingElementsUITests' do - # Pods for testing - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' - - end - -end diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj b/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj deleted file mode 100644 index ae700fd2..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj +++ /dev/null @@ -1,756 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXBuildFile section */ - 84209722024122879D341F43 /* Pods_UpdateDataUsingElementsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */; }; - A9BAC9FE1923AA2AF6EBDD4A /* Pods_UpdateDataUsingElements.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */; }; - F9F0ED650060535674C02713 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 8EE9B1D32F5D47F600E676C7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 8EE9B1B42F5D47EF00E676C7 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8EE9B1BB2F5D47EF00E676C7; - remoteInfo = UpdateDataUsingElements; - }; - 8EE9B1DD2F5D47F600E676C7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 8EE9B1B42F5D47EF00E676C7 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8EE9B1BB2F5D47EF00E676C7; - remoteInfo = UpdateDataUsingElements; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElementsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElementsTests.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests.release.xcconfig"; sourceTree = ""; }; - 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig"; sourceTree = ""; }; - 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements.release.xcconfig"; sourceTree = ""; }; - 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElementsTests.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests.debug.xcconfig"; sourceTree = ""; }; - 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UpdateDataUsingElements.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UpdateDataUsingElementsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UpdateDataUsingElementsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig"; sourceTree = ""; }; - DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements.debug.xcconfig"; sourceTree = ""; }; - F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 8EE9B1E42F5D47F600E676C7 /* Exceptions for "UpdateDataUsingElements" folder in "UpdateDataUsingElements" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 8EE9B1E42F5D47F600E676C7 /* Exceptions for "UpdateDataUsingElements" folder in "UpdateDataUsingElements" target */, - ); - path = UpdateDataUsingElements; - sourceTree = ""; - }; - 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - path = UpdateDataUsingElementsTests; - sourceTree = ""; - }; - 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - path = UpdateDataUsingElementsUITests; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - 8EE9B1B92F5D47EF00E676C7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A9BAC9FE1923AA2AF6EBDD4A /* Pods_UpdateDataUsingElements.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8EE9B1CF2F5D47F600E676C7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 84209722024122879D341F43 /* Pods_UpdateDataUsingElementsTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8EE9B1D92F5D47F600E676C7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F9F0ED650060535674C02713 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 27820800536265CC8997BEDA /* Pods */ = { - isa = PBXGroup; - children = ( - DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */, - 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */, - D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */, - 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */, - 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */, - 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 46291EA2BCEB4448675D0DA8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */, - A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */, - 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 8EE9B1B32F5D47EF00E676C7 = { - isa = PBXGroup; - children = ( - 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */, - 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */, - 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, - 8EE9B1BD2F5D47EF00E676C7 /* Products */, - 27820800536265CC8997BEDA /* Pods */, - 46291EA2BCEB4448675D0DA8 /* Frameworks */, - ); - sourceTree = ""; - }; - 8EE9B1BD2F5D47EF00E676C7 /* Products */ = { - isa = PBXGroup; - children = ( - 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */, - 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */, - 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8EE9B1E52F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElements" */; - buildPhases = ( - 0F8A093288F474159BEDE644 /* [CP] Check Pods Manifest.lock */, - 8EE9B1B82F5D47EF00E676C7 /* Sources */, - 8EE9B1B92F5D47EF00E676C7 /* Frameworks */, - 8EE9B1BA2F5D47EF00E676C7 /* Resources */, - E938BD775733C7F94C478863 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */, - ); - name = UpdateDataUsingElements; - productName = UpdateDataUsingElements; - productReference = 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */; - productType = "com.apple.product-type.application"; - }; - 8EE9B1D12F5D47F600E676C7 /* UpdateDataUsingElementsTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8EE9B1EA2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsTests" */; - buildPhases = ( - 8B6ABD965E2972F8AC0F597B /* [CP] Check Pods Manifest.lock */, - 8EE9B1CE2F5D47F600E676C7 /* Sources */, - 8EE9B1CF2F5D47F600E676C7 /* Frameworks */, - 8EE9B1D02F5D47F600E676C7 /* Resources */, - 87992577B2A3D0F9B4A74C66 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 8EE9B1D42F5D47F600E676C7 /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */, - ); - name = UpdateDataUsingElementsTests; - productName = UpdateDataUsingElementsTests; - productReference = 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 8EE9B1DB2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8EE9B1ED2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsUITests" */; - buildPhases = ( - E3F917F11A21B04E3AE532A9 /* [CP] Check Pods Manifest.lock */, - 8EE9B1D82F5D47F600E676C7 /* Sources */, - 8EE9B1D92F5D47F600E676C7 /* Frameworks */, - 8EE9B1DA2F5D47F600E676C7 /* Resources */, - 7ED4115DECEAE1F715BC3D6F /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 8EE9B1DE2F5D47F600E676C7 /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, - ); - name = UpdateDataUsingElementsUITests; - productName = UpdateDataUsingElementsUITests; - productReference = 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 8EE9B1B42F5D47EF00E676C7 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1640; - LastUpgradeCheck = 1640; - TargetAttributes = { - 8EE9B1BB2F5D47EF00E676C7 = { - CreatedOnToolsVersion = 16.4; - }; - 8EE9B1D12F5D47F600E676C7 = { - CreatedOnToolsVersion = 16.4; - TestTargetID = 8EE9B1BB2F5D47EF00E676C7; - }; - 8EE9B1DB2F5D47F600E676C7 = { - CreatedOnToolsVersion = 16.4; - TestTargetID = 8EE9B1BB2F5D47EF00E676C7; - }; - }; - }; - buildConfigurationList = 8EE9B1B72F5D47EF00E676C7 /* Build configuration list for PBXProject "UpdateDataUsingElements" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 8EE9B1B32F5D47EF00E676C7; - minimizedProjectReferenceProxies = 1; - packageReferences = ( - 8EE9B1F42F5D489500E676C7 /* XCRemoteSwiftPackageReference "skyflow-iOS" */, - ); - preferredProjectObjectVersion = 77; - productRefGroup = 8EE9B1BD2F5D47EF00E676C7 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */, - 8EE9B1D12F5D47F600E676C7 /* UpdateDataUsingElementsTests */, - 8EE9B1DB2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 8EE9B1BA2F5D47EF00E676C7 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8EE9B1D02F5D47F600E676C7 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8EE9B1DA2F5D47F600E676C7 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 0F8A093288F474159BEDE644 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElements-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 7ED4115DECEAE1F715BC3D6F /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 87992577B2A3D0F9B4A74C66 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 8B6ABD965E2972F8AC0F597B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElementsTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E3F917F11A21B04E3AE532A9 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E938BD775733C7F94C478863 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 8EE9B1B82F5D47EF00E676C7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8EE9B1CE2F5D47F600E676C7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8EE9B1D82F5D47F600E676C7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 8EE9B1D42F5D47F600E676C7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; - targetProxy = 8EE9B1D32F5D47F600E676C7 /* PBXContainerItemProxy */; - }; - 8EE9B1DE2F5D47F600E676C7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; - targetProxy = 8EE9B1DD2F5D47F600E676C7 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 8EE9B1E62F5D47F600E676C7 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = UpdateDataUsingElements/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElements; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 8EE9B1E72F5D47F600E676C7 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = UpdateDataUsingElements/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElements; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 8EE9B1E82F5D47F600E676C7 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 8EE9B1E92F5D47F600E676C7 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 8EE9B1EB2F5D47F600E676C7 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UpdateDataUsingElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UpdateDataUsingElements"; - }; - name = Debug; - }; - 8EE9B1EC2F5D47F600E676C7 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UpdateDataUsingElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UpdateDataUsingElements"; - }; - name = Release; - }; - 8EE9B1EE2F5D47F600E676C7 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = UpdateDataUsingElements; - }; - name = Debug; - }; - 8EE9B1EF2F5D47F600E676C7 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = UpdateDataUsingElements; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 8EE9B1B72F5D47EF00E676C7 /* Build configuration list for PBXProject "UpdateDataUsingElements" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8EE9B1E82F5D47F600E676C7 /* Debug */, - 8EE9B1E92F5D47F600E676C7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 8EE9B1E52F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElements" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8EE9B1E62F5D47F600E676C7 /* Debug */, - 8EE9B1E72F5D47F600E676C7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 8EE9B1EA2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8EE9B1EB2F5D47F600E676C7 /* Debug */, - 8EE9B1EC2F5D47F600E676C7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 8EE9B1ED2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8EE9B1EE2F5D47F600E676C7 /* Debug */, - 8EE9B1EF2F5D47F600E676C7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 8EE9B1F42F5D489500E676C7 /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.24.2; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - }; - rootObject = 8EE9B1B42F5D47EF00E676C7 /* Project object */; -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata b/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 28e92450..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift deleted file mode 100644 index aa40b1e6..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// UpdateDataUsingElements -// -// Created by Bharti Sagar on 08/03/26. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 23058801..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift deleted file mode 100644 index b08d2305..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (Any) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift deleted file mode 100644 index 75a0efee..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleTokenProvider: TokenProvider { - public func getBearerToken(_ apiCallback: Skyflow.Callback) { - if let url = URL(string: "") { - let session = URLSession(configuration: .default) - let task = session.dataTask(with: url) { data, _, error in - if error != nil { - print(error!) - return - } - if let safeData = data { - do { - let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] - if let accessToken = x?["accessToken"] { - apiCallback.onSuccess(accessToken) - } - } catch { - print("access token wrong format") - } - } - } - task.resume() - } - } -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist deleted file mode 100644 index dd3c9afd..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift deleted file mode 100644 index 4b948ce6..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields? - let table: String - let skyflow_id: String? -} - -struct Fields: Codable { - let cardholder_name: String? - let card_number: String? - let expiry_month: String? - let expiry_year: String? - let cvv: String? - let skyflow_id: String? -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift deleted file mode 100644 index 6f4093b7..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SceneDelegate.swift -// UpdateDataUsingElements -// -// Created by Bharti Sagar on 08/03/26. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift deleted file mode 100644 index 6639ec61..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import UIKit -import Skyflow - -class ViewController: UIViewController { - var retryCount = 0 - private var skyflow: Skyflow.Client? - private var container: Skyflow.Container? - private var b: UIButton? - - private var stackView: UIStackView! - - override func loadView() { - let view = UIView() - view.backgroundColor = .white - view.translatesAutoresizingMaskIntoConstraints = false - self.view = view - } - - override func viewDidLoad() { - super.viewDidLoad() - - let tokenProvider = ExampleTokenProvider() - - let config = Skyflow.Configuration( - vaultID: "", - vaultURL: "", - tokenProvider: tokenProvider, - options: Skyflow.Options( - logLevel: Skyflow.LogLevel.DEBUG - ) - ) - - self.skyflow = Skyflow.initialize(config) - - if self.skyflow != nil { - let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1,2,2], styles: Styles(base: Style(borderColor: UIColor.gray) ), errorTextStyles: Styles(base: Style(textColor: UIColor.red) ))) - self.container = container - self.stackView = UIStackView() - - let baseStyle = Skyflow.Style( - cornerRadius: 2, - padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), - borderWidth: 1, - textAlignment: .left, - textColor: .blue - ) - - let focusStyle = Skyflow.Style(borderColor: .blue) - - let completedStyle = Skyflow.Style(textColor: UIColor.green) - - let invalidStyle = Skyflow.Style(textColor: UIColor.red) - - let styles = Skyflow.Styles( - base: baseStyle, - complete: completedStyle, - focus: focusStyle, - invalid: invalidStyle - ) - - let collectCardNumberInput = Skyflow.CollectElementInput( - table: "credit_cards", - column: "card_number", - inputStyles: styles, - label: "Card Number", - placeholder: "4111-1111-1111-1111", - type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input - - ) - let collectNameInput = Skyflow.CollectElementInput( - table: "credit_cards", - column: "cardholder_name", - inputStyles: styles, - label: "Card Holder Name", - placeholder: "John Doe", - type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input - ) - let collectCVVInput = Skyflow.CollectElementInput( - table: "credit_cards", - column: "cvv", - inputStyles: styles, - label: "CVV", - placeholder: "***", - type: .CVV, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input - ) - let collectExpMonthInput = Skyflow.CollectElementInput( - table: "credit_cards", - column: "expiry_month", - inputStyles: styles, - label: "Expiration Month", - placeholder: "MM", - type: .EXPIRATION_MONTH, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input - - ) - let collectExpYearInput = Skyflow.CollectElementInput( - table: "credit_cards", - column: "expiry_year", - inputStyles: styles, - label: "Expiration Year", - placeholder: "YYYY", - type: .EXPIRATION_YEAR, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input - - ) - let requiredOption = Skyflow.CollectElementOptions(required: true) - let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) - let collectName = container?.create(input: collectNameInput, options: requiredOption) - let collectCVV = container?.create(input: collectCVVInput, options: requiredOption) - let collectExpMonth = container?.create(input: collectExpMonthInput, options: requiredOption) - let collectExpYear = container?.create(input: collectExpYearInput, options: requiredOption) - let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) - collectButton.backgroundColor = .blue - collectButton.setTitle("Submit", for: .normal) - collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) - - do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) - } catch { - print(error) - } - - stackView.addArrangedSubview(collectButton) - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 10 - stackView.alignment = .fill - stackView.translatesAutoresizingMaskIntoConstraints = false - let scrollView = UIScrollView(frame: .zero) - scrollView.isScrollEnabled = true - scrollView.backgroundColor = .white - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - scrollView.topAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: 20 - ).isActive = true - scrollView.leftAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.leftAnchor, - constant: 10 - ).isActive = true - scrollView.rightAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.rightAnchor, - constant: -10 - ).isActive = true - scrollView.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor - ).isActive = true - scrollView.addSubview(stackView) - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true - stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true - stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - } - } - - @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) - } - internal func updateSuccess(_ response: SuccessResponse) { - print(response) - retryCount = 0 - print("Successfully got response:", response) - } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future - retryCount += 1 - submitForm() - } - print("Failed Operation", error) - } - -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift deleted file mode 100644 index eada6780..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// UpdateDataUsingElementsTests.swift -// UpdateDataUsingElementsTests -// -// Created by Bharti Sagar on 08/03/26. -// - -import XCTest -@testable import UpdateDataUsingElements - -final class UpdateDataUsingElementsTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift deleted file mode 100644 index a921baab..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// UpdateDataUsingElementsUITests.swift -// UpdateDataUsingElementsUITests -// -// Created by Bharti Sagar on 08/03/26. -// - -import XCTest - -final class UpdateDataUsingElementsUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } -} diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift deleted file mode 100644 index 81b9cb73..00000000 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// UpdateDataUsingElementsUITestsLaunchTests.swift -// UpdateDataUsingElementsUITests -// -// Created by Bharti Sagar on 08/03/26. -// - -import XCTest - -final class UpdateDataUsingElementsUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard b/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard b/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift b/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift deleted file mode 100644 index 6ea1a8a6..00000000 --- a/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardnumber: String - let cvv: String - let skyflow_id: String -} diff --git a/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/Validations/Validations/Assets.xcassets/Contents.json b/Samples/Validations/Validations/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Samples/Validations/Validations/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard b/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/Validations/Validations/Base.lproj/Main.storyboard b/Samples/Validations/Validations/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/Samples/Validations/Validations/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Skyflow-flowvault-ios.podspec b/Skyflow-flowvault-ios.podspec new file mode 100644 index 00000000..f12efd9c --- /dev/null +++ b/Skyflow-flowvault-ios.podspec @@ -0,0 +1,46 @@ +Pod::Spec.new do |spec| + + spec.name = "Skyflow-flowvault-ios" + + # Consumers import the SDK as `import SkyflowFlowVaultIOS` (Swift module names + # cannot contain hyphens). + spec.module_name = "SkyflowFlowVaultIOS" + + spec.version = "1.26.0-beta.1" + + spec.summary = "skyflow-flowvault-iOS" + + spec.description = "Skyflow FlowVault iOS SDK (v2 API)" + + spec.homepage = "https://github.com/skyflowapi/skyflow-iOS.git" + + spec.license = { :type => "MIT", :file => "LICENSE" } + + spec.author = { "Skyflow" => "service-ops@skyflow.com" } + + spec.swift_version = '5.9' + + spec.platform = :ios, "13.0" + + spec.ios.deployment_target = "13.0" + + # Both SDKs share one version line: a single x.y.z tag releases both pods. + spec.source = { :git => "https://github.com/skyflowapi/skyflow-iOS.git", :tag => "1.26.0-beta.1" } + + # CocoaPods compiles the shared core and the FlowVault contract layer into a + # single module, so the SPM-only re-export shim is excluded. + spec.source_files = "Sources/SkyflowCore/**/*.{swift}", "Sources/SkyflowFlowVaultIOS/**/*.{swift}" + + spec.exclude_files = "Sources/SkyflowFlowVaultIOS/Exports.swift" + + # `package` access-level declarations (shared across the SPM targets) require a + # package name when compiled outside SPM. + spec.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name skyflow_ios_sdk' } + + # Bundle name intentionally stays 'Skyflow' — the shared core's asset lookups + # load "Skyflow.bundle" (see SkyflowAssetBundle), and keeping one name for both + # pods avoids any build-configuration dependence. The bundle is internal to the + # pod and never seen by SDK consumers. + spec.resource_bundles = {'Skyflow' => ['Sources/SkyflowCore/Resources/**/*.{xcassets}'] } + +end diff --git a/Skyflow.podspec b/Skyflow.podspec index 88887bb6..a2d2223e 100644 --- a/Skyflow.podspec +++ b/Skyflow.podspec @@ -2,11 +2,11 @@ Pod::Spec.new do |spec| spec.name = "Skyflow" - spec.version = "1.25.1" + spec.version = "1.26.0-beta.1" spec.summary = "skyflow-iOS" - spec.description = "Skyflow iOS SDK" + spec.description = "Skyflow iOS SDK (legacy v1 vault API)" spec.homepage = "https://github.com/skyflowapi/skyflow-iOS.git" @@ -14,17 +14,24 @@ Pod::Spec.new do |spec| spec.author = { "Skyflow" => "service-ops@skyflow.com" } - spec.swift_version = '5.0' + spec.swift_version = '5.9' spec.platform = :ios, "13.0" spec.ios.deployment_target = "13.0" - spec.source = { :git => "https://github.com/skyflowapi/skyflow-iOS.git", :tag => "1.25.1" } + spec.source = { :git => "https://github.com/skyflowapi/skyflow-iOS.git", :tag => "1.26.0-beta.1" } - spec.source_files = "Sources/Skyflow/**/*.{swift}" + # CocoaPods compiles the shared core and the legacy contract layer into a single + # module, so the SPM-only re-export shim is excluded. + spec.source_files = "Sources/SkyflowCore/**/*.{swift}", "Sources/Skyflow/**/*.{swift}" - spec.resource_bundles = {'Skyflow' => ['Sources/Skyflow/Resources/**/*.{xcassets}'] } + spec.exclude_files = "Sources/Skyflow/Exports.swift" -end + # `package` access-level declarations (shared across the SPM targets) require a + # package name when compiled outside SPM. + spec.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name skyflow_ios_sdk' } + + spec.resource_bundles = {'Skyflow' => ['Sources/SkyflowCore/Resources/**/*.{xcassets}'] } +end diff --git a/Sources/Skyflow/Exports.swift b/Sources/Skyflow/Exports.swift new file mode 100644 index 00000000..7d80d99d --- /dev/null +++ b/Sources/Skyflow/Exports.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Re-export the shared core so SDK consumers only need `import Skyflow`. + +@_exported import SkyflowCore diff --git a/Sources/Skyflow/core/Init.swift b/Sources/Skyflow/Init.swift similarity index 87% rename from Sources/Skyflow/core/Init.swift rename to Sources/Skyflow/Init.swift index 365bb9c2..5be55583 100644 --- a/Sources/Skyflow/core/Init.swift +++ b/Sources/Skyflow/Init.swift @@ -7,5 +7,6 @@ import Foundation public func initialize(_ skyflowConfig: Configuration) -> Client { + SDK_NAME = "skyflow-iOS" return Client(skyflowConfig) } diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Contents.json b/Sources/Skyflow/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Sources/Skyflow/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/Skyflow/Version.swift b/Sources/Skyflow/Version.swift deleted file mode 100644 index a89bf081..00000000 --- a/Sources/Skyflow/Version.swift +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// -// File.swift -// -// -// Created by Bharti Sagar on 19/05/23. -// - -import Foundation - -var LangAndVersion = "iOS SDK v\(SDK_VERSION)" - -var SDK_VERSION = "1.25.1" diff --git a/Sources/Skyflow/collect/CollectAPICallback.swift b/Sources/Skyflow/collect/CollectAPICallback.swift index 74cae186..19a5ac1b 100644 --- a/Sources/Skyflow/collect/CollectAPICallback.swift +++ b/Sources/Skyflow/collect/CollectAPICallback.swift @@ -42,7 +42,7 @@ internal class CollectAPICallback: Callback { if hasInsert { group.enter() - guard let url = URL(string: self.apiClient.vaultURL + self.apiClient.vaultID) else { + guard let url = URL(string: self.apiClient.legacyVaultURL + self.apiClient.vaultID) else { self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) return } @@ -75,7 +75,7 @@ internal class CollectAPICallback: Callback { group.leave() continue } - let urlString = self.apiClient.vaultURL + self.apiClient.vaultID + "/" + table + "/" + skyflowID + let urlString = self.apiClient.legacyVaultURL + self.apiClient.vaultID + "/" + table + "/" + skyflowID guard let url = URL(string: urlString) else { group.leave() requestError?.append(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) diff --git a/Sources/Skyflow/collect/CollectContainer.swift b/Sources/Skyflow/collect/CollectContainerOperations.swift similarity index 84% rename from Sources/Skyflow/collect/CollectContainer.swift rename to Sources/Skyflow/collect/CollectContainerOperations.swift index 16bb9035..1a122452 100644 --- a/Sources/Skyflow/collect/CollectContainer.swift +++ b/Sources/Skyflow/collect/CollectContainerOperations.swift @@ -2,26 +2,12 @@ * Copyright (c) 2022 Skyflow */ -// Implementation of Container Interface for Collect the records +// Legacy (v1) implementation of the collect() operation. import Foundation import UIKit -public class CollectContainer: ContainerProtocol {} - public extension Container { - func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: CollectContainer { - var tempContextOptions = self.skyflow.contextOptions - tempContextOptions.interface = .COLLECT_CONTAINER - let skyflowElement = TextField(input: input, options: options!, contextOptions: tempContextOptions, elements: elements) - elements.append(skyflowElement) - let uuid = NSUUID().uuidString - self.skyflow.elementLookup[uuid] = skyflowElement - skyflowElement.uuid = uuid - Log.info(message: .CREATED_ELEMENT, values: [input.label == "" ? "collect" : input.label], contextOptions: tempContextOptions) - return skyflowElement - } - func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER @@ -29,7 +15,7 @@ public extension Container { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -104,7 +90,7 @@ public extension Container { } private func checkElement(element: TextField) -> ErrorCodes? { - if element.collectInput.table.isEmpty { + if element.collectInput.tableName.isEmpty { return .EMPTY_TABLE_NAME_IN_COLLECT() } if element.collectInput.column.isEmpty { @@ -141,3 +127,9 @@ public extension Container { return nil } } + +public extension Container { + func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: CollectContainer { + return makeCollectElement(input: input.data, options: options?.data) + } +} diff --git a/Sources/Skyflow/collect/CollectRequestBody.swift b/Sources/Skyflow/collect/CollectRequestBody.swift index b687f141..fd9accd9 100644 --- a/Sources/Skyflow/collect/CollectRequestBody.swift +++ b/Sources/Skyflow/collect/CollectRequestBody.swift @@ -126,7 +126,7 @@ internal class CollectRequestBody { let tableName = element.tableName! let columnName = element.columnName! let value = element.getValue() - let skyflowID = element.skyflowID // Assumes TextField has this property + let skyflowID = element.skyflowId // Assumes TextField has this property if let skyflowID = skyflowID, !skyflowID.isEmpty { if updatePayload[skyflowID] != nil { diff --git a/Sources/Skyflow/collect/ComposableContainerOperations.swift b/Sources/Skyflow/collect/ComposableContainerOperations.swift new file mode 100644 index 00000000..19ec78fb --- /dev/null +++ b/Sources/Skyflow/collect/ComposableContainerOperations.swift @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Legacy (v1) implementation of the composable container's collect() operation. + +import Foundation +import UIKit + +public extension Container { + + func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { + var tempContextOptions = self.skyflow.contextOptions + tempContextOptions.interface = .COMPOSABLE_CONTAINER + if self.skyflow.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.skyflow.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + var errors = "" + var errorCode: ErrorCodes? + Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) + + for element in self.elements { + errorCode = checkComposableElement(element: element) + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + + let state = element.getState() + let error = state["validationError"] + if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { + errors += element.columnName + " is empty" + "\n" + element.updateErrorMessage() + } + if !(state["isValid"] as! Bool) { + errors += "for " + element.columnName + " " + (error as! String) + "\n" + } + if element.isFirstResponder { + element.resignFirstResponder() + } + } + + if errors != "" { + callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + + return + } + if options?.additionalFields != nil { + if options?.additionalFields!["records"] == nil { + errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { + if additionalFieldEntries.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + for (index, record) in additionalFieldEntries.enumerated() { + errorCode = checkComposableRecord(record: record, index: index) + if errorCode != nil { + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + } else { + errorCode = .INVALID_RECORDS_TYPE() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + } + let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + if options?.upsert != nil { + if icOptions.validateUpsert() { + return; + } + } + if records != nil { + let logCallback = LogCallback(clientCallback: callback, contextOptions: self.skyflow.contextOptions, + onSuccessHandler: { + Log.info(message: .COLLECT_SUBMIT_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.skyflow.apiClient.postAndUpdate(records: records!, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) + } + } + + private func checkComposableElement(element: TextField) -> ErrorCodes? { + if element.collectInput.tableName.isEmpty { + return .EMPTY_TABLE_NAME_IN_COLLECT() + } + if element.collectInput.column.isEmpty { + return .EMPTY_COLUMN_NAME_IN_COLLECT() + } + if !element.isMounted() { + return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + } + + return nil + } + + private func checkComposableRecord(record: [String: Any], index: Int) -> ErrorCodes? { + if record["table"] == nil { + return .TABLE_KEY_ERROR(value: "\(index)") + } + if !(record["table"] is String) { + return .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } + if (record["table"] as? String == "") { + return .EMPTY_TABLE_NAME() + } + if record["fields"] == nil { + return .FIELDS_KEY_ERROR(value: "\(index)") + } + if !(record["fields"] is [String: Any]) { + return .INVALID_FIELDS_TYPE(value: "\(index)") + } + let fields = record["fields"] as! [String: Any] + if (fields.isEmpty){ + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + + return nil + } + +} + +public extension Container { + func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: ComposableContainer { + return makeComposableElement(input: input.data, options: options?.data) + } +} diff --git a/Sources/Skyflow/collect/InsertAPICallback.swift b/Sources/Skyflow/collect/InsertAPICallback.swift index f2fe26f2..1f872bbf 100644 --- a/Sources/Skyflow/collect/InsertAPICallback.swift +++ b/Sources/Skyflow/collect/InsertAPICallback.swift @@ -22,7 +22,7 @@ internal class InsertAPICallback: Callback { self.contextOptions = contextOptions } internal func onSuccess(_ responseBody: Any) { - guard let url = URL(string: self.apiClient.vaultURL + self.apiClient.vaultID) else { + guard let url = URL(string: self.apiClient.legacyVaultURL + self.apiClient.vaultID) else { self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) return } diff --git a/Sources/Skyflow/core/APIClient.swift b/Sources/Skyflow/core/APIClientLegacy.swift similarity index 59% rename from Sources/Skyflow/core/APIClient.swift rename to Sources/Skyflow/core/APIClientLegacy.swift index 3cfd6ef9..44322538 100644 --- a/Sources/Skyflow/core/APIClient.swift +++ b/Sources/Skyflow/core/APIClientLegacy.swift @@ -2,61 +2,14 @@ * Copyright (c) 2022 Skyflow */ - -// Class used for generating different type of requests, req body etc for API making an API call +// Legacy (v1) contract operations on the shared APIClient. import Foundation -internal class APIClient { - var vaultID: String - var vaultURL: String - var tokenProvider: TokenProvider - var token: String = "" - - internal init(vaultID: String, vaultURL: String, tokenProvider: TokenProvider) { - self.vaultID = vaultID - self.vaultURL = vaultURL - self.tokenProvider = tokenProvider - } - - internal func isTokenValid() -> Bool { - if token == "" { - return false - } - - let components = token.components(separatedBy: ".") - - if components.count < 2 { - return false - } - - var payload64 = components[1] - - while payload64.count % 4 != 0 { - payload64 += "=" - } - - let payloadData = Data(base64Encoded: payload64, - options: .ignoreUnknownCharacters)! - - do { - let json = try JSONSerialization.jsonObject(with: payloadData, options: []) as! [String: Any] - let exp = json["exp"] as! Int - let expDate = Date(timeIntervalSince1970: TimeInterval(exp)) - - return expDate.compare(Date()) == .orderedDescending - } catch { - return false - } - } - - internal func getAccessToken(callback: Callback, contextOptions: ContextOptions) { - if !isTokenValid() { - let tokenApiCallback = TokenAPICallback(callback: callback, apiClient: self, contextOptions: contextOptions) - tokenProvider.getBearerToken(tokenApiCallback) - } else { - callback.onSuccess(token) - } +extension APIClient { + // Base URL for v1 vault endpoints; legacy callbacks append the vault ID and paths. + package var legacyVaultURL: String { + return vaultURL + "v1/vaults/" } internal func postAndUpdate(records: [String: Any], callback: Callback, options: ICOptions, contextOptions: ContextOptions) { @@ -85,7 +38,7 @@ internal class APIClient { temp["tableName"] = record["table"] temp["method"] = "POST" temp["quorum"] = true - + if options.tokens { var temp2: [String: Any] = [:] temp2["method"] = "GET" @@ -114,18 +67,21 @@ internal class APIClient { } return uniqueColumn; } - + internal func get(records: [RevealRequestRecord], callback: Callback, contextOptions: ContextOptions) { - let revealApiCallback = RevealAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, contextOptions: contextOptions) + let revealApiCallback = RevealAPICallback(callback: callback, apiClient: self, connectionUrl: (legacyVaultURL + vaultID), records: records, contextOptions: contextOptions) self.getAccessToken(callback: revealApiCallback, contextOptions: contextOptions) } +} - internal func getById(records: [GetByIdRecord], callback: Callback, contextOptions: ContextOptions) { - let revealByIdApiCallback = RevealByIDAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, contextOptions: contextOptions) +// v1 get/getById wire calls (moved from core: legacy-contract only). +extension APIClient { + package func getById(records: [GetByIdRecord], callback: Callback, contextOptions: ContextOptions) { + let revealByIdApiCallback = RevealByIDAPICallback(callback: callback, apiClient: self, connectionUrl: (legacyVaultURL + vaultID), records: records, contextOptions: contextOptions) self.getAccessToken(callback: revealByIdApiCallback, contextOptions: contextOptions) } - internal func getRecord(records: [GetRecord], callback: Callback,getOptions: GetOptions, contextOptions: ContextOptions) { - let getApiCallback = GetAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, getOptions: getOptions,contextOptions: contextOptions) + package func getRecord(records: [GetRecord], callback: Callback, getOptions: GetOptions, contextOptions: ContextOptions) { + let getApiCallback = GetAPICallback(callback: callback, apiClient: self, connectionUrl: (legacyVaultURL + vaultID), records: records, getOptions: getOptions, contextOptions: contextOptions) self.getAccessToken(callback: getApiCallback, contextOptions: contextOptions) } } diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift index c9e51761..7d318dec 100644 --- a/Sources/Skyflow/core/Client.swift +++ b/Sources/Skyflow/core/Client.swift @@ -2,434 +2,21 @@ * Copyright (c) 2022 Skyflow */ -// Implementation of Skyflow Client class +// Implementation of the Skyflow Client class (legacy v1 contract). +// The v1 operations live in ClientOperations.swift as an extension of this +// class. Future enhancements specific to this SDK go here (or in extensions +// in this target) without affecting the FlowVault SDK. import Foundation -public class Client { - var vaultID: String - var apiClient: APIClient - var vaultURL: String - var contextOptions: ContextOptions - var elementLookup: [String: Any] = [:] - +public class Client: ClientBase { public init(_ skyflowConfig: Configuration) { - self.vaultID = skyflowConfig.vaultID - self.vaultURL = skyflowConfig.vaultURL.hasSuffix("/") ? skyflowConfig.vaultURL + "v1/vaults/" : skyflowConfig.vaultURL + "/v1/vaults/" - self.apiClient = APIClient(vaultID: skyflowConfig.vaultID, vaultURL: self.vaultURL, tokenProvider: skyflowConfig.tokenProvider) - self.contextOptions = ContextOptions(logLevel: skyflowConfig.options!.logLevel, env: skyflowConfig.options!.env, interface: .CLIENT) - Log.info(message: .CLIENT_INITIALIZED, contextOptions: self.contextOptions) - } - - public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .INSERT - Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/v1/vaults/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) - var errorCode: ErrorCodes? - - if records["records"] == nil { - errorCode = .RECORDS_KEY_ERROR() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) - if let recordEntries = records["records"] as? [[String: Any]] { - for (index, record) in recordEntries.enumerated() { - if record["table"] != nil { - if !(record["table"] is String) { - errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } else { - if (record["table"] as! String).isEmpty { - errorCode = .EMPTY_TABLE_NAME() - } else { - if record["fields"] != nil { - if !(record["fields"] is [String: Any]) { - errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") - break - } - let fields = record["fields"] as! [String: Any] - if fields.isEmpty { - errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") - } - } else { - errorCode = .FIELDS_KEY_ERROR(value: "\(index)") - } - } - } - } else { - errorCode = .TABLE_KEY_ERROR(value: "\(index)") - } - } - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } else { - if options.upsert != nil { - if icOptions.validateUpsert() { - return; - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.post(records: records, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - - public func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { - if options != nil { - // Set options - } - - if T.self == CollectContainer.self { - Log.info(message: .COLLECT_CONTAINER_CREATED, contextOptions: self.contextOptions) - return Container(skyflow: self) - } - - if T.self == RevealContainer.self { - Log.info(message: .REVEAL_CONTAINER_CREATED, contextOptions: self.contextOptions) - return Container(skyflow: self) - } - if T.self == ComposableContainer.self { - return Container(skyflow: self, options: options) - } - return nil - } - - public func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .DETOKENIZE - func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { - if token["redaction"] != nil { - guard let _ = token["redaction"] as? RedactionType else { - return .INVALID_REDACTION_TYPE() - } - } - if token["token"] == nil { - return .ID_KEY_ERROR() - } else { - guard let _ = token["token"] as? String else { - return .INVALID_TOKEN_TYPE(value: "\(index)") - } - } - return nil - } - - Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/v1/vaults/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) - } - - if let tokens = records["records"] as? [[String: Any]] { - var list: [RevealRequestRecord] = [] - if tokens.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index,token) in tokens.enumerated() { - let errorCode = checkRecord(token: token, index: index) - if errorCode == nil, let id = token["token"] as? String { - if token["redaction"] == nil{ - list.append(RevealRequestRecord(token: id, redaction: RedactionType.PLAIN_TEXT.rawValue)) - } else if let redaction = token["redaction"] as? RedactionType{ - list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) - } - } else { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - - public func getById(records: [String: Any], callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .GETBYID - Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/v1/vaults/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_GET_BY_ID_INPUT, contextOptions: tempContextOptions) - - func checkEntry(entry: [String: Any], index: Int) -> ErrorCodes? { - if entry.isEmpty { - return .EMPTY_RECORDS_OBJECT() - } - if entry["ids"] == nil { - return .MISSING_KEY_IDS(value: "\(index)") - } - if !(entry["ids"] is [String]) { - return .INVALID_IDS_TYPE() - } - if ((entry["ids"] as? [String])?.count == 0) { - return .EMPTY_IDS(value: "\(index)") - } - let ids = entry["ids"] as! [String] - for id in ids { - if (id == "") { - return .EMPTY_ID_VALUE(value: "\(index)") - } - } - if entry["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(entry["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if ((entry["table"] as? String) == "") { - return .EMPTY_TABLE_NAME() - } - if entry["redaction"] == nil { - return .REDACTION_KEY_ERROR(value: "\(index)") - } - if (entry["redaction"] as? RedactionType) != nil { - return nil - } else { - return .INVALID_REDACTION_TYPE() - } - } - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) //Check - } - - if let entries = records["records"] as? [[String: Any]] { - var list: [GetByIdRecord] = [] - if entries.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index, entry) in entries.enumerated() { - let errorCode = checkEntry(entry: entry, index: index) - if errorCode != nil { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } else { - if let ids = entry["ids"] as? [String], let table = entry["table"] as? String, let redaction = entry["redaction"] as? RedactionType { - list.append(GetByIdRecord(ids: ids, table: table, redaction: redaction.rawValue)) - } - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .GET_BY_ID_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.getById(records: list, callback: logCallback, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - public func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .GET - Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/v1/vaults/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - if let entries = records["records"] as? [[String: Any]] { - var list: [GetRecord] = [] - if entries.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index,entry) in entries.enumerated() { - let errorCode = validateGetRecords(entry: entry, getOptions: options, index: index) - if errorCode != nil { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } else { - if let ids = entry["ids"] as? [String], let table = entry["table"] as? String { - if let redaction = entry["redaction"] as? RedactionType { - list.append(GetRecord(ids: ids, table: table, redaction: redaction.rawValue)) - } else - { - list.append(GetRecord(ids: ids, table: table)) - } - - } - if let columnValues = entry["columnValues"] as? [String], let table = entry["table"] as? String, let columnName = entry["columnName"] as? String, let redaction = entry["redaction"] as? RedactionType { - list.append(GetRecord(columnValues: columnValues, table: table, columnName: columnName, redaction: redaction.rawValue)) - } - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, onSuccessHandler: { - Log.info(message: .GET_SUCCESS, contextOptions: tempContextOptions) - }, onFailureHandler: { - - }) - self.apiClient.getRecord(records: list, callback: logCallback, getOptions: options, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - internal func validateGetRecords(entry: [String: Any], getOptions: GetOptions, index: Int) -> ErrorCodes? { - if entry.isEmpty { - return .EMPTY_RECORDS_OBJECT() - } - if (entry["ids"] != nil ){ - if !(entry["ids"] is [String]) { - return .INVALID_IDS_TYPE() - } - if ((entry["ids"] as? [String])?.count == 0) { - return .EMPTY_IDS(value: "\(index)") - } - let ids = entry["ids"] as! [String] - for id in ids { - if (id == "") { - return .EMPTY_ID_VALUE(value: "\(index)") - } - } - } - if entry["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(entry["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if ((entry["table"] as? String) == "") { - return .EMPTY_TABLE_NAME() - } - - if( getOptions.tokens == true ){ - if (entry["columnName"] != nil || entry["columnValues"] != nil){ - return .TOKENS_GET_COLUMN_NOT_SUPPPORTED() - - } - if (entry["redaction"] as? RedactionType) != nil { - return .REDACTION_WITH_TOKEN_NOT_SUPPORTED() - } - } else { - if entry["redaction"] == nil { - return .REDACTION_KEY_ERROR(value: "\(index)") - } else if (entry["redaction"] as? RedactionType) == nil { - return .INVALID_REDACTION_TYPE() - } - } - - if(entry["columnName"] == nil){ - if ((entry["ids"] == nil) && (entry["columnValues"] == nil)){ - return .MISSING_IDS_OR_COLUMN_VALUES_IN_GET() - } - } else if (entry["columnName"] != nil && entry["columnValues"] == nil){ - return .MISSING_RECORD_COLUMN_VALUE() - } else if !(entry["columnName"] is String){ - return .INVALID_COLUMN_NAME(value: "\(index)") - } else if ((entry["ids"] != nil) && (entry["columnName"] != nil)){ - return .SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED() - } - if (entry["columnValues"] != nil){ - if ((entry["columnValues"] as? [String])?.count == 0) { - return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") - } - if !(entry["columnValues"] is [String]) { - return .INVALID_COLUMN_VALUES_IN_GET() - } - if ((entry["columnValues"] as? [String])?.count == 0) { - return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") - } - let columnValues = entry["columnValues"] as! [String] - for columnValue in columnValues { - if (columnValue == "") { - return .EMPTY_COLUMN_VALUE(value: "\(index)") - } - } - if( entry["columnName"] == nil ){ - return .MISSING_COLUMN_NAME() - } else if((entry["columnName"] as? String) == ""){ - return .EMPTY_COLUMN_NAME() - } - } - - return nil - } - - private func callRevealOnFailure(callback: Callback, errorObject: Error) { - let result = ["errors": [errorObject]] - callback.onFailure(result) - } - - internal func createDetokenizeRecords(_ IDsToTokens: [String: String]) -> [String: [[String: String]]]{ - var records = [] as [[String : String]] - var index = 0 - for (_, token) in IDsToTokens { - records.append(["token": token]) - index += 1 - } - - return ["records": records] + super.init(skyflowConfig.data) } } -internal class LogCallback: Callback { - var clientCallback: Callback - var contextOptions: ContextOptions - var onSuccessHandler: () -> Void - var onFailureHandler: () -> Void - - public init(clientCallback: Callback, contextOptions: ContextOptions, onSuccessHandler: @escaping () -> Void, onFailureHandler: @escaping () -> Void) { - self.clientCallback = clientCallback - self.contextOptions = contextOptions - self.onSuccessHandler = onSuccessHandler - self.onFailureHandler = onFailureHandler - } - - func onSuccess(_ responseBody: Any) { - self.onSuccessHandler() - clientCallback.onSuccess(responseBody) - } - - func onFailure(_ error: Any) { - self.onFailureHandler() - clientCallback.onFailure(error) +public extension Client { + func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { + return makeContainer(type: type, options: options?.data) } } diff --git a/Sources/Skyflow/core/ClientOperations.swift b/Sources/Skyflow/core/ClientOperations.swift new file mode 100644 index 00000000..141ef6e5 --- /dev/null +++ b/Sources/Skyflow/core/ClientOperations.swift @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Legacy (v1) contract operations on this SDK's Client class (declared in Client.swift). + +import Foundation + +extension Client { + public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .INSERT + Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) + if self.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) + var errorCode: ErrorCodes? + + if records["records"] == nil { + errorCode = .RECORDS_KEY_ERROR() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) + if let recordEntries = records["records"] as? [[String: Any]] { + for (index, record) in recordEntries.enumerated() { + if record["table"] != nil { + if !(record["table"] is String) { + errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } else { + if (record["table"] as! String).isEmpty { + errorCode = .EMPTY_TABLE_NAME() + } else { + if record["fields"] != nil { + if !(record["fields"] is [String: Any]) { + errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") + break + } + let fields = record["fields"] as! [String: Any] + if fields.isEmpty { + errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") + } + } else { + errorCode = .FIELDS_KEY_ERROR(value: "\(index)") + } + } + } + } else { + errorCode = .TABLE_KEY_ERROR(value: "\(index)") + } + } + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } else { + if options.upsert != nil { + if icOptions.validateUpsert() { + return; + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.post(records: records, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) + } + } else { + errorCode = .INVALID_RECORDS_TYPE() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + + public func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .DETOKENIZE + func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { + if token["redaction"] != nil { + guard let _ = token["redaction"] as? RedactionType else { + return .INVALID_REDACTION_TYPE() + } + } + if token["token"] == nil { + return .ID_KEY_ERROR() + } else { + guard let _ = token["token"] as? String else { + return .INVALID_TOKEN_TYPE(value: "\(index)") + } + } + return nil + } + + Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) + if self.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) + } + + if let tokens = records["records"] as? [[String: Any]] { + var list: [RevealRequestRecord] = [] + if tokens.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index,token) in tokens.enumerated() { + let errorCode = checkRecord(token: token, index: index) + if errorCode == nil, let id = token["token"] as? String { + if token["redaction"] == nil{ + list.append(RevealRequestRecord(token: id, redaction: RedactionType.PLAIN_TEXT.rawValue)) + } else if let redaction = token["redaction"] as? RedactionType{ + list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) + } + } else { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + + public func getById(records: [String: Any], callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .GETBYID + Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) + if self.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_GET_BY_ID_INPUT, contextOptions: tempContextOptions) + + func checkEntry(entry: [String: Any], index: Int) -> ErrorCodes? { + if entry.isEmpty { + return .EMPTY_RECORDS_OBJECT() + } + if entry["ids"] == nil { + return .MISSING_KEY_IDS(value: "\(index)") + } + if !(entry["ids"] is [String]) { + return .INVALID_IDS_TYPE() + } + if ((entry["ids"] as? [String])?.count == 0) { + return .EMPTY_IDS(value: "\(index)") + } + let ids = entry["ids"] as! [String] + for id in ids { + if (id == "") { + return .EMPTY_ID_VALUE(value: "\(index)") + } + } + if entry["table"] == nil { + return .TABLE_KEY_ERROR(value: "\(index)") + } + if !(entry["table"] is String) { + return .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } + if ((entry["table"] as? String) == "") { + return .EMPTY_TABLE_NAME() + } + if entry["redaction"] == nil { + return .REDACTION_KEY_ERROR(value: "\(index)") + } + if (entry["redaction"] as? RedactionType) != nil { + return nil + } else { + return .INVALID_REDACTION_TYPE() + } + } + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) //Check + } + + if let entries = records["records"] as? [[String: Any]] { + var list: [GetByIdRecord] = [] + if entries.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index, entry) in entries.enumerated() { + let errorCode = checkEntry(entry: entry, index: index) + if errorCode != nil { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } else { + if let ids = entry["ids"] as? [String], let table = entry["table"] as? String, let redaction = entry["redaction"] as? RedactionType { + list.append(GetByIdRecord(ids: ids, table: table, redaction: redaction.rawValue)) + } + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .GET_BY_ID_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.getById(records: list, callback: logCallback, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + public func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .GET + Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) + if self.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + if let entries = records["records"] as? [[String: Any]] { + var list: [GetRecord] = [] + if entries.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index,entry) in entries.enumerated() { + let errorCode = validateGetRecords(entry: entry, getOptions: options, index: index) + if errorCode != nil { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } else { + if let ids = entry["ids"] as? [String], let table = entry["table"] as? String { + if let redaction = entry["redaction"] as? RedactionType { + list.append(GetRecord(ids: ids, table: table, redaction: redaction.rawValue)) + } else + { + list.append(GetRecord(ids: ids, table: table)) + } + + } + if let columnValues = entry["columnValues"] as? [String], let table = entry["table"] as? String, let columnName = entry["columnName"] as? String, let redaction = entry["redaction"] as? RedactionType { + list.append(GetRecord(columnValues: columnValues, table: table, columnName: columnName, redaction: redaction.rawValue)) + } + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, onSuccessHandler: { + Log.info(message: .GET_SUCCESS, contextOptions: tempContextOptions) + }, onFailureHandler: { + + }) + self.apiClient.getRecord(records: list, callback: logCallback, getOptions: options, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + + internal func createDetokenizeRecords(_ IDsToTokens: [String: String]) -> [String: [[String: String]]]{ + var records = [] as [[String : String]] + var index = 0 + for (_, token) in IDsToTokens { + records.append(["token": token]) + index += 1 + } + + return ["records": records] + } +} + +// Validation for the v1 get/getById records dictionaries (moved from core: +// only the legacy contract exposes these operations). +extension Client { + package func validateGetRecords(entry: [String: Any], getOptions: GetOptions, index: Int) -> ErrorCodes? { + if entry.isEmpty { + return .EMPTY_RECORDS_OBJECT() + } + if (entry["ids"] != nil ){ + if !(entry["ids"] is [String]) { + return .INVALID_IDS_TYPE() + } + if ((entry["ids"] as? [String])?.count == 0) { + return .EMPTY_IDS(value: "\(index)") + } + let ids = entry["ids"] as! [String] + for id in ids { + if (id == "") { + return .EMPTY_ID_VALUE(value: "\(index)") + } + } + } + if entry["table"] == nil { + return .TABLE_KEY_ERROR(value: "\(index)") + } + if !(entry["table"] is String) { + return .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } + if ((entry["table"] as? String) == "") { + return .EMPTY_TABLE_NAME() + } + + if( getOptions.tokens == true ){ + if (entry["columnName"] != nil || entry["columnValues"] != nil){ + return .TOKENS_GET_COLUMN_NOT_SUPPPORTED() + + } + if (entry["redaction"] as? RedactionType) != nil { + return .REDACTION_WITH_TOKEN_NOT_SUPPORTED() + } + } else { + if entry["redaction"] == nil { + return .REDACTION_KEY_ERROR(value: "\(index)") + } else if (entry["redaction"] as? RedactionType) == nil { + return .INVALID_REDACTION_TYPE() + } + } + + if(entry["columnName"] == nil){ + if ((entry["ids"] == nil) && (entry["columnValues"] == nil)){ + return .MISSING_IDS_OR_COLUMN_VALUES_IN_GET() + } + } else if (entry["columnName"] != nil && entry["columnValues"] == nil){ + return .MISSING_RECORD_COLUMN_VALUE() + } else if !(entry["columnName"] is String){ + return .INVALID_COLUMN_NAME(value: "\(index)") + } else if ((entry["ids"] != nil) && (entry["columnName"] != nil)){ + return .SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED() + } + if (entry["columnValues"] != nil){ + if ((entry["columnValues"] as? [String])?.count == 0) { + return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") + } + if !(entry["columnValues"] is [String]) { + return .INVALID_COLUMN_VALUES_IN_GET() + } + if ((entry["columnValues"] as? [String])?.count == 0) { + return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") + } + let columnValues = entry["columnValues"] as! [String] + for columnValue in columnValues { + if (columnValue == "") { + return .EMPTY_COLUMN_VALUE(value: "\(index)") + } + } + if( entry["columnName"] == nil ){ + return .MISSING_COLUMN_NAME() + } else if((entry["columnName"] as? String) == ""){ + return .EMPTY_COLUMN_NAME() + } + } + + return nil + } +} diff --git a/Sources/Skyflow/core/Configuration.swift b/Sources/Skyflow/core/Configuration.swift index 13adbf04..df1e4992 100644 --- a/Sources/Skyflow/core/Configuration.swift +++ b/Sources/Skyflow/core/Configuration.swift @@ -2,20 +2,16 @@ * Copyright (c) 2022 Skyflow */ -// Configure Skyflow, implementation for Skyflow.Configuration +// Configure Skyflow, implementation for the Configuration object. +// Contract-identical in both SDKs today; declared per SDK so either product +// can add configuration options independently in the future. import Foundation public struct Configuration { - var vaultID: String - var vaultURL: String - var tokenProvider: TokenProvider - var options: Options? + package var data: BaseConfiguration public init(vaultID: String = "", vaultURL: String = "", tokenProvider: TokenProvider, options: Options? = Options()) { - self.vaultID = vaultID - self.vaultURL = vaultURL - self.tokenProvider = tokenProvider - self.options = options + self.data = BaseConfiguration(vaultID: vaultID, vaultURL: vaultURL, tokenProvider: tokenProvider, options: options) } } diff --git a/Sources/Skyflow/core/ContainerOptions.swift b/Sources/Skyflow/core/ContainerOptions.swift new file mode 100644 index 00000000..9fa39144 --- /dev/null +++ b/Sources/Skyflow/core/ContainerOptions.swift @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Options for composable containers (layout/styles). Contract-identical in +// both SDKs today; declared per SDK so either product can evolve +// independently. + +import Foundation + +public struct ContainerOptions { + package var data: BaseContainerOptions + + public init(){ + self.data = BaseContainerOptions() + } + + public init(layout: [Int], styles: Styles? = Styles(), errorTextStyles: Styles? = Styles()) { + self.data = BaseContainerOptions(layout: layout, styles: styles, errorTextStyles: errorTextStyles) + } + +} diff --git a/Sources/Skyflow/core/ContextOptions.swift b/Sources/Skyflow/core/ContextOptions.swift deleted file mode 100644 index b8d2174a..00000000 --- a/Sources/Skyflow/core/ContextOptions.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// -// File.swift -// -// -// Created by Akhil Anil Mangala on 07/10/21. -// - -import Foundation - -internal struct ContextOptions { - var logLevel: LogLevel - var env: Env - var interface: InterfaceName - - internal init(logLevel: LogLevel = .ERROR, env: Env = .PROD, interface: InterfaceName = .EMPTY) { - self.logLevel = logLevel - self.env = env - self.interface = interface - } -} diff --git a/Sources/Skyflow/core/container/Container.swift b/Sources/Skyflow/core/container/Container.swift deleted file mode 100644 index 53158c5b..00000000 --- a/Sources/Skyflow/core/container/Container.swift +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// -// File.swift -// -// -// Created by Akhil Anil Mangala on 27/07/21. -// - -import Foundation - -public class Container { - internal var skyflow: Client - internal var elements: [TextField] = [] - internal var revealElements: [Label] = [] - internal var containerOptions: ContainerOptions? = nil - - internal init(skyflow: Client) { - self.skyflow = skyflow - } - internal init(skyflow: Client, options: ContainerOptions? = nil){ - self.containerOptions = options - self.skyflow = skyflow - } -} diff --git a/Sources/Skyflow/core/container/ContainerOptions.swift b/Sources/Skyflow/core/container/ContainerOptions.swift deleted file mode 100644 index ead33e98..00000000 --- a/Sources/Skyflow/core/container/ContainerOptions.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// -// File.swift -// -// -// Created by Akhil Anil Mangala on 27/07/21. -// - -import Foundation - -public struct ContainerOptions { - var layout: [Int] - var styles: Styles? - var errorTextStyles: Styles? - - public init(){ - layout = [0] - } - - public init(layout: [Int], styles: Styles? = Styles(), errorTextStyles: Styles? = Styles()) { - self.layout = layout - self.styles = styles - self.errorTextStyles = errorTextStyles - } - -} diff --git a/Sources/Skyflow/elements/CollectElementInput.swift b/Sources/Skyflow/elements/CollectElementInput.swift new file mode 100644 index 00000000..8099e41d --- /dev/null +++ b/Sources/Skyflow/elements/CollectElementInput.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// An Object that describes SkyflowInputField (legacy contract: `table:` and +// `skyflowID:` argument labels, exactly as released). A struct, as on main: +// elements store their own copy, so reusing one input never shares state. + +import Foundation + +public struct CollectElementInput { + package var data: BaseCollectElementInput + + public init(table: String = "", column: String = "", + inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", + placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + self.data = BaseCollectElementInput(tableName: table, column: column, inputStyles: inputStyles!, labelStyles: labelStyles!, errorTextStyles: errorTextStyles!, iconStyles: iconStyles!, label: label!, placeholder: placeholder!, type: nil, validations: validations, skyflowId: skyflowID) + } + + public init(table: String = "", column: String = "", + inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", + placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + self.data = BaseCollectElementInput(tableName: table, column: column, inputStyles: inputStyles!, labelStyles: labelStyles!, errorTextStyles: errorTextStyles!, iconStyles: iconStyles!, label: label!, placeholder: placeholder!, type: type, validations: validations, skyflowId: skyflowID) + } + + @available(*, deprecated, message: "altText param is deprecated") + public init(table: String = "", column: String = "", + inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", + placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + self.data = BaseCollectElementInput(tableName: table, column: column, inputStyles: inputStyles!, labelStyles: labelStyles!, errorTextStyles: errorTextStyles!, iconStyles: iconStyles!, label: label!, placeholder: placeholder!, type: type, validations: validations, skyflowId: skyflowID) + } +} + +public extension TextField { + func update(update: CollectElementInput) { + self.update(data: update.data) + } +} diff --git a/Sources/Skyflow/elements/CollectElementOptions.swift b/Sources/Skyflow/elements/CollectElementOptions.swift new file mode 100644 index 00000000..13f18716 --- /dev/null +++ b/Sources/Skyflow/elements/CollectElementOptions.swift @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// An Object that describes Options parameter for SkyflowInputField. +// Contract-identical in both SDKs today; declared per SDK so either product +// can add or remove options independently in the future. + +import Foundation +#if os(iOS) +import UIKit +#endif + +public struct CollectElementOptions { + package var data: BaseCollectElementOptions + + public init(required: Bool? = false, enableCardIcon: Bool = true, format: String = "mm/yy", translation: [ Character: String ]? = nil, enableCopy: Bool = false, cardMetaData: [ String: Any]? = nil) { + self.data = BaseCollectElementOptions(required: required!, enableCardIcon: enableCardIcon, format: format, translation: translation, enableCopy: enableCopy, cardMetaData: cardMetaData) + } +} + +public extension TextField { + func update(updateOptions: CollectElementOptions) { + self.update(optionsData: updateOptions.data) + } +} diff --git a/Sources/Skyflow/elements/core/CollectElementInput.swift b/Sources/Skyflow/elements/core/CollectElementInput.swift deleted file mode 100644 index 419d6b8d..00000000 --- a/Sources/Skyflow/elements/core/CollectElementInput.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// An Object that describes SkyflowInputField - -import Foundation - -public struct CollectElementInput { - var table: String - var column: String - var inputStyles: Styles - var labelStyles: Styles - var errorTextStyles: Styles - var iconStyles: Styles - var label: String - var placeholder: String - var type: ElementType? - var validations: ValidationSet - var skyflowID: String - - public init(table: String = "", column: String = "", - inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { - self.table = table - self.column = column - self.inputStyles = inputStyles! - self.labelStyles = labelStyles! - self.errorTextStyles = errorTextStyles! - self.iconStyles = iconStyles! - self.label = label! - self.placeholder = placeholder! - self.validations = validations - self.skyflowID = skyflowID! - } - - public init(table: String = "", column: String = "", - inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { - self.table = table - self.column = column - self.inputStyles = inputStyles! - self.labelStyles = labelStyles! - self.errorTextStyles = errorTextStyles! - self.iconStyles = iconStyles! - self.label = label! - self.placeholder = placeholder! - self.type = type - self.validations = validations - self.skyflowID = skyflowID! - } - - @available(*, deprecated, message: "altText param is deprecated") - public init(table: String = "", column: String = "", - inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { - self.table = table - self.column = column - self.inputStyles = inputStyles! - self.labelStyles = labelStyles! - self.errorTextStyles = errorTextStyles! - self.iconStyles = iconStyles! - self.label = label! - self.placeholder = placeholder! - self.type = type - self.validations = validations - self.skyflowID = skyflowID! - } -} diff --git a/Sources/Skyflow/reveal/GetByIdRecord.swift b/Sources/Skyflow/reveal/GetByIdRecord.swift index 1dae151f..0d294487 100644 --- a/Sources/Skyflow/reveal/GetByIdRecord.swift +++ b/Sources/Skyflow/reveal/GetByIdRecord.swift @@ -11,11 +11,11 @@ import Foundation -struct GetByIdRecord { +package struct GetByIdRecord { var ids: [String] var table: String var redaction: String - init(ids: [String], table: String, redaction: String) { + package init(ids: [String], table: String, redaction: String) { self.ids = ids self.table = table self.redaction = redaction diff --git a/Sources/Skyflow/reveal/GetRecord.swift b/Sources/Skyflow/reveal/GetRecord.swift index 72b71c16..84763299 100644 --- a/Sources/Skyflow/reveal/GetRecord.swift +++ b/Sources/Skyflow/reveal/GetRecord.swift @@ -7,23 +7,23 @@ import Foundation -struct GetRecord { +package struct GetRecord { var ids: [String]? var table: String var redaction: String? var columnName: String? var columnValues: [String]? - init(ids: [String], table: String, redaction: String) { + package init(ids: [String], table: String, redaction: String) { self.ids = ids self.table = table self.redaction = redaction } - init(ids: [String], table: String) { + package init(ids: [String], table: String) { self.ids = ids self.table = table } - init(columnValues: [String], table: String, columnName: String, redaction: String) { + package init(columnValues: [String], table: String, columnName: String, redaction: String) { self.columnValues = columnValues self.table = table self.columnName = columnName diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainerOperations.swift similarity index 79% rename from Sources/Skyflow/reveal/RevealContainer.swift rename to Sources/Skyflow/reveal/RevealContainerOperations.swift index 57f97bc9..2a97a380 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainerOperations.swift @@ -2,29 +2,11 @@ * Copyright (c) 2022 Skyflow */ -/* - * Implementation of Reveal container which helps in creating - * the reveal element, revealing the tokenized or redacted text - */ +// Legacy (v1) implementation of the reveal() operation (per-token redaction). import Foundation -public class RevealContainer: ContainerProtocol { -} - public extension Container { - func create(input: RevealElementInput, options: RevealElementOptions? = RevealElementOptions()) -> Label where T: RevealContainer { - var tempContextOptions = self.skyflow.contextOptions - tempContextOptions.interface = .REVEAL_CONTAINER - let revealElement = Label(input: input, options: options!) - revealElements.append(revealElement) - let uuid = NSUUID().uuidString - self.skyflow.elementLookup[uuid] = revealElement - revealElement.uuid = uuid - Log.info(message: .CREATED_ELEMENT, values: [input.label == "" ? "reveal" : input.label], contextOptions: tempContextOptions) - return revealElement - } - func reveal(callback: Callback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER @@ -32,7 +14,7 @@ public extension Container { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -76,3 +58,9 @@ public extension Container { } } } + +public extension Container { + func create(input: RevealElementInput, options: RevealElementOptions? = RevealElementOptions()) -> Label where T: RevealContainer { + return makeRevealElement(input: input.data, options: options?.data) + } +} diff --git a/Sources/Skyflow/reveal/RevealElementInput.swift b/Sources/Skyflow/reveal/RevealElementInput.swift index d0da3195..809215a7 100644 --- a/Sources/Skyflow/reveal/RevealElementInput.swift +++ b/Sources/Skyflow/reveal/RevealElementInput.swift @@ -2,36 +2,20 @@ * Copyright (c) 2022 Skyflow */ -// Object for reveal element +// Object for reveal element (legacy contract: supports per-token redaction). +// A struct, as on main: elements store their own copy, so reusing one input +// never shares state. import Foundation public struct RevealElementInput { - internal var token: String - internal var inputStyles: Styles? - internal var labelStyles: Styles? - internal var errorTextStyles: Styles? - internal var label: String - internal var redaction: RedactionType - internal var altText: String + package var data: BaseRevealElementInput public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, redaction: RedactionType = .PLAIN_TEXT, altText: String = "") { - self.token = token - self.inputStyles = inputStyles - self.labelStyles = labelStyles - self.errorTextStyles = errorTextStyles - self.label = label - self.redaction = redaction - self.altText = altText + self.data = BaseRevealElementInput(token: token, inputStyles: inputStyles, labelStyles: labelStyles, errorTextStyles: errorTextStyles, label: label, redaction: redaction, altText: altText) } public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "") { - self.token = token - self.inputStyles = inputStyles - self.labelStyles = labelStyles - self.errorTextStyles = errorTextStyles - self.label = label - self.redaction = .PLAIN_TEXT - self.altText = altText + self.data = BaseRevealElementInput(token: token, inputStyles: inputStyles, labelStyles: labelStyles, errorTextStyles: errorTextStyles, label: label, redaction: .PLAIN_TEXT, altText: altText) } } diff --git a/Sources/Skyflow/reveal/RevealElementOptions.swift b/Sources/Skyflow/reveal/RevealElementOptions.swift index 106a559b..959b537c 100644 --- a/Sources/Skyflow/reveal/RevealElementOptions.swift +++ b/Sources/Skyflow/reveal/RevealElementOptions.swift @@ -2,29 +2,15 @@ * Copyright (c) 2022 Skyflow */ +// Options for reveal elements. Contract-identical in both SDKs today; +// declared per SDK so either product can evolve independently. + import Foundation public struct RevealElementOptions { - var format: String? - var translation: [ Character: String ]? - var enableCopy: Bool? - + package var data: BaseRevealElementOptions public init(format: String? = nil, translation: [ Character: String ]? = nil, enableCopy: Bool? = false) { - self.format = format - self.translation = translation - self.enableCopy = enableCopy - if (self.translation != nil){ - for (_, value) in self.translation! { - if value == "" { - var contextOptions = ContextOptions() - contextOptions.interface = .REVEAL_CONTAINER - contextOptions.logLevel = .WARN - Log.warn(message: .EMPTY_TRANSLATION_VALUE, values: [], contextOptions: contextOptions) - } - } - - } + self.data = BaseRevealElementOptions(format: format, translation: translation, enableCopy: enableCopy) } - } diff --git a/Sources/Skyflow/skyflow_iOS.swift b/Sources/Skyflow/skyflow_iOS.swift deleted file mode 100644 index 226327f6..00000000 --- a/Sources/Skyflow/skyflow_iOS.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -struct skyflow_iOS { - var text = "Hello, World!" -} diff --git a/Sources/Skyflow/BaseElement.swift b/Sources/SkyflowCore/BaseElement.swift similarity index 100% rename from Sources/Skyflow/BaseElement.swift rename to Sources/SkyflowCore/BaseElement.swift diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json diff --git a/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png b/Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/checkmark.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/checkmark.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/checkmark.imageset/checkmark.png b/Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/checkmark.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/checkmark.imageset/checkmark.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/checkmark.png diff --git a/Sources/Skyflow/Resources/Assets.xcassets/dropdown.imageset/Contents.json b/Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/Contents.json similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/dropdown.imageset/Contents.json rename to Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/Contents.json diff --git a/Sources/Skyflow/Resources/Assets.xcassets/dropdown.imageset/dropdown.png b/Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/dropdown.png similarity index 100% rename from Sources/Skyflow/Resources/Assets.xcassets/dropdown.imageset/dropdown.png rename to Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/dropdown.png diff --git a/Sources/SkyflowCore/Version.swift b/Sources/SkyflowCore/Version.swift new file mode 100644 index 00000000..0448e46a --- /dev/null +++ b/Sources/SkyflowCore/Version.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// +// File.swift +// +// +// Created by Bharti Sagar on 19/05/23. +// + +import Foundation + +// Both SDK products share this version (single source of truth, kept in sync +// with both podspecs by scripts/bump_version.sh). Each product stamps its own +// SDK_NAME in initialize(); name and version feed error-message prefixes and +// the sdk_name_version vault metric. The default name matches the legacy +// product so directly constructing Client(_:) still reports correctly there. +package var SDK_NAME = "skyflow-iOS" + +package var SDK_VERSION = "1.26.0-beta.1" + +var LangAndVersion: String { "iOS SDK v\(SDK_VERSION)" } diff --git a/Sources/SkyflowCore/collect/CollectContainer.swift b/Sources/SkyflowCore/collect/CollectContainer.swift new file mode 100644 index 00000000..387ea9f7 --- /dev/null +++ b/Sources/SkyflowCore/collect/CollectContainer.swift @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Collect container marker type and the shared element factory. +// The public create(input:options:) and the contract-specific collect() +// operation are defined per SDK target; create() forwards here. + +import Foundation +import UIKit + +public class CollectContainer: ContainerProtocol {} + +extension Container { + package func makeCollectElement(input: BaseCollectElementInput, options: BaseCollectElementOptions? = BaseCollectElementOptions()) -> TextField where T: CollectContainer { + var tempContextOptions = self.skyflow.contextOptions + tempContextOptions.interface = .COLLECT_CONTAINER + let skyflowElement = TextField(input: input, options: options!, contextOptions: tempContextOptions, elements: elements) + elements.append(skyflowElement) + let uuid = NSUUID().uuidString + self.skyflow.elementLookup[uuid] = skyflowElement + skyflowElement.uuid = uuid + Log.info(message: .CREATED_ELEMENT, values: [input.label == "" ? "collect" : input.label], contextOptions: tempContextOptions) + return skyflowElement + } +} diff --git a/Sources/Skyflow/collect/TokenAPICallback.swift b/Sources/SkyflowCore/collect/TokenAPICallback.swift similarity index 100% rename from Sources/Skyflow/collect/TokenAPICallback.swift rename to Sources/SkyflowCore/collect/TokenAPICallback.swift diff --git a/Sources/Skyflow/composable/AutoshiftAllowed.swift b/Sources/SkyflowCore/composable/AutoshiftAllowed.swift similarity index 100% rename from Sources/Skyflow/composable/AutoshiftAllowed.swift rename to Sources/SkyflowCore/composable/AutoshiftAllowed.swift diff --git a/Sources/Skyflow/composable/ComposableContainer.swift b/Sources/SkyflowCore/composable/ComposableContainer.swift similarity index 62% rename from Sources/Skyflow/composable/ComposableContainer.swift rename to Sources/SkyflowCore/composable/ComposableContainer.swift index 5da7784c..ee0d25cc 100644 --- a/Sources/Skyflow/composable/ComposableContainer.swift +++ b/Sources/SkyflowCore/composable/ComposableContainer.swift @@ -2,7 +2,8 @@ * Copyright (c) 2022 Skyflow */ -// Implementation of Composable Container Interface for Collect the records +// Composable container marker type, element creation, event wiring, and layout. +// The contract-specific collect() operation is defined per SDK target. import Foundation import UIKit @@ -10,8 +11,7 @@ import UIKit public class ComposableContainer: ContainerProtocol {} public extension Container { - - func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: ComposableContainer { + package func makeComposableElement(input: BaseCollectElementInput, options: BaseCollectElementOptions? = BaseCollectElementOptions()) -> TextField where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER @@ -23,7 +23,7 @@ public extension Container { Log.info(message: .CREATED_ELEMENT, values: [input.label == "" ? "composable" : input.label], contextOptions: tempContextOptions) return skyflowElement } - + func on(eventName: EventName, handler: @escaping () -> Void) { if (eventName == EventName.SUBMIT){ for element in elements { @@ -35,7 +35,7 @@ public extension Container { internal func createRows(from elements: [Int], numberOfRows: Int) -> [[Int]] { var number = Array(repeating: 0, count: elements.count) var result = [[Int]]() - + for i in 0.. 0 { number[i] = elements[i] + number[i-1] @@ -69,9 +69,9 @@ public extension Container { return "" } let subarray = array[startIndex...endIndex] - + let concatenatedString = subarray.joined(separator: "") - + return concatenatedString } internal func createDynamicViews(layout: [Int]) -> UIView { @@ -83,13 +83,13 @@ public extension Container { let rowWiseError = createRows(from: layout, numberOfRows: layout.count) var elementCount = 0 let layoutArray = layout - + for i in layoutArray.indices { let childView = UIView() labelArray[i] = UILabel() parentView.addSubview(childView) parentView.addSubview(labelArray[i]) - + labelArray[i].translatesAutoresizingMaskIntoConstraints = false labelArray[i].textColor = containerOptions?.errorTextStyles?.base?.textColor ?? .none labelArray[i].font = containerOptions?.errorTextStyles?.base?.font ?? .none @@ -110,14 +110,14 @@ public extension Container { if containerOptions?.styles?.base?.width != nil { childView.widthAnchor.constraint(equalToConstant: (containerOptions?.styles?.base?.width)!).isActive = true } - + elements[elementCount].translatesAutoresizingMaskIntoConstraints = false - + if layoutArray[i] > 1 && elementCount >= 1 && j > 0 { elements[elementCount].leadingAnchor.constraint(equalTo: elements[elementCount-1].trailingAnchor, constant: 20.0).isActive = true elements[elementCount].centerYAnchor.constraint(equalTo: childView.centerYAnchor).isActive = true - + } else if j == 0 { elements[elementCount].centerYAnchor.constraint(equalTo: childView.centerYAnchor).isActive = true elements[elementCount].leftAnchor.constraint(equalTo: childView.leftAnchor, constant: 6.0).isActive = true @@ -134,7 +134,7 @@ public extension Container { labelArray[i].font = self.containerOptions?.errorTextStyles?.focus?.font ?? self.containerOptions?.errorTextStyles?.base?.font ?? .none labelArray[i].textAlignment = self.containerOptions?.errorTextStyles?.focus?.textAlignment ?? self.containerOptions?.errorTextStyles?.base?.textAlignment ?? .left } - + element.onEndEditing = { if element.errorMessage.text == "" { errorList[element.elements.count] = "" @@ -162,15 +162,15 @@ public extension Container { childView.topAnchor.constraint(equalTo: previousLabel?.bottomAnchor ?? previousChildView?.bottomAnchor ?? parentView.topAnchor, constant: 10.0).isActive = true childView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor).isActive = true childView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor).isActive = true - + labelArray[i].translatesAutoresizingMaskIntoConstraints = false labelArray[i].leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 6.0).isActive = true labelArray[i].trailingAnchor.constraint(equalTo: parentView.trailingAnchor, constant: -6.0).isActive = true labelArray[i].topAnchor.constraint(equalTo: childView.bottomAnchor, constant: 5.0).isActive = true - + previousChildView = childView previousLabel = labelArray[i] - + } previousChildView?.trailingAnchor.constraint(equalTo: parentView.trailingAnchor).isActive = true parentView.bottomAnchor.constraint(equalTo: previousLabel?.bottomAnchor ?? previousChildView?.bottomAnchor ?? parentView.bottomAnchor, constant: 10.0).isActive = true @@ -178,12 +178,12 @@ public extension Container { return parentView } - + func getComposableView() throws -> UIView { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER var totalCount = 0 - + if let options = containerOptions { if (options.layout.count == 0) { throw SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "\(ErrorCodes.EMPTY_COMPOSABLE_LAYOUT_ARRAY().description)" ]) @@ -202,128 +202,4 @@ public extension Container { let view = createDynamicViews(layout: (containerOptions?.layout)!) return view } - - - func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { - var tempContextOptions = self.skyflow.contextOptions - tempContextOptions.interface = .COMPOSABLE_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/v1/vaults/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - var errors = "" - var errorCode: ErrorCodes? - Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) - - for element in self.elements { - errorCode = checkElement(element: element) - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - - let state = element.getState() - let error = state["validationError"] - if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { - errors += element.columnName + " is empty" + "\n" - element.updateErrorMessage() - } - if !(state["isValid"] as! Bool) { - errors += "for " + element.columnName + " " + (error as! String) + "\n" - } - if element.isFirstResponder { - element.resignFirstResponder() - } - } - - if errors != "" { - callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) - - return - } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - } - let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) - if options?.upsert != nil { - if icOptions.validateUpsert() { - return; - } - } - if records != nil { - let logCallback = LogCallback(clientCallback: callback, contextOptions: self.skyflow.contextOptions, - onSuccessHandler: { - Log.info(message: .COLLECT_SUBMIT_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.skyflow.apiClient.postAndUpdate(records: records!, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) - } - } - - private func checkElement(element: TextField) -> ErrorCodes? { - if element.collectInput.table.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) - } - - return nil - } - - private func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { - return .EMPTY_TABLE_NAME() - } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ - return .EMPTY_FIELDS_KEY(value: "\(index)") - } - - return nil - } - } - diff --git a/Sources/SkyflowCore/core/APIClient.swift b/Sources/SkyflowCore/core/APIClient.swift new file mode 100644 index 00000000..dcdca324 --- /dev/null +++ b/Sources/SkyflowCore/core/APIClient.swift @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + + +// Contract-agnostic API client: bearer token lifecycle and shared vault operations. +// Contract-specific operations (insert/collect/detokenize) live in the SDK targets +// as extensions on this class. + +import Foundation + +package class APIClient { + package var vaultID: String + // Normalized base vault URL (always ends with "/"); SDK targets append their + // contract-specific paths (v1/vaults/... or v2/...). + package var vaultURL: String + package var tokenProvider: TokenProvider + package var token: String = "" + + package init(vaultID: String, vaultURL: String, tokenProvider: TokenProvider) { + self.vaultID = vaultID + self.vaultURL = vaultURL + self.tokenProvider = tokenProvider + } + + package func isTokenValid() -> Bool { + if token == "" { + return false + } + + let components = token.components(separatedBy: ".") + + if components.count < 2 { + return false + } + + var payload64 = components[1] + + while payload64.count % 4 != 0 { + payload64 += "=" + } + + let payloadData = Data(base64Encoded: payload64, + options: .ignoreUnknownCharacters)! + + do { + let json = try JSONSerialization.jsonObject(with: payloadData, options: []) as! [String: Any] + let exp = json["exp"] as! Int + let expDate = Date(timeIntervalSince1970: TimeInterval(exp)) + + return expDate.compare(Date()) == .orderedDescending + } catch { + return false + } + } + + package func getAccessToken(callback: Callback, contextOptions: ContextOptions) { + if !isTokenValid() { + let tokenApiCallback = TokenAPICallback(callback: callback, apiClient: self, contextOptions: contextOptions) + tokenProvider.getBearerToken(tokenApiCallback) + } else { + callback.onSuccess(token) + } + } + +} diff --git a/Sources/SkyflowCore/core/BaseConfiguration.swift b/Sources/SkyflowCore/core/BaseConfiguration.swift new file mode 100644 index 00000000..97f5f200 --- /dev/null +++ b/Sources/SkyflowCore/core/BaseConfiguration.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared storage for the client configuration. Each SDK product defines its +// own public `BaseConfiguration` struct exposing that contract's initializer +// (identical in both contracts today); ClientBase consumes this value once at +// initialization and does not retain it. + +import Foundation + +package struct BaseConfiguration { + package var vaultID: String + package var vaultURL: String + package var tokenProvider: TokenProvider + package var options: Options? + + package init(vaultID: String = "", vaultURL: String = "", tokenProvider: TokenProvider, options: Options? = Options()) { + self.vaultID = vaultID + self.vaultURL = vaultURL + self.tokenProvider = tokenProvider + self.options = options + } +} diff --git a/Sources/SkyflowCore/core/ClientBase.swift b/Sources/SkyflowCore/core/ClientBase.swift new file mode 100644 index 00000000..62d85380 --- /dev/null +++ b/Sources/SkyflowCore/core/ClientBase.swift @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared base for the Skyflow client. Each SDK product defines its own public +// `Client` subclass where that contract's operations (insert/detokenize/get/ +// getById, ...) and any future SDK-specific enhancements live; the base holds +// shared state, container creation, and validation helpers. Applications can +// see the base type name but cannot construct, subclass, or use it (package init/members). + +import Foundation + +open class ClientBase { + package var vaultID: String + package var apiClient: APIClient + // Normalized base vault URL (always ends with "/"). Empty configuration input + // normalizes to "/" — SDK extensions use that to detect a missing vault URL. + package var vaultURL: String + package var contextOptions: ContextOptions + package var elementLookup: [String: Any] = [:] + + package init(_ skyflowConfig: BaseConfiguration) { + self.vaultID = skyflowConfig.vaultID + let normalizedBaseURL = skyflowConfig.vaultURL.hasSuffix("/") ? skyflowConfig.vaultURL : skyflowConfig.vaultURL + "/" + self.vaultURL = normalizedBaseURL + self.apiClient = APIClient(vaultID: skyflowConfig.vaultID, vaultURL: normalizedBaseURL, tokenProvider: skyflowConfig.tokenProvider) + self.contextOptions = ContextOptions(logLevel: skyflowConfig.options!.logLevel, env: skyflowConfig.options!.env, interface: .CLIENT) + Log.info(message: .CLIENT_INITIALIZED, contextOptions: self.contextOptions) + } + + // The public container(type:options:) taking each SDK's ContainerOptions + // struct is defined per SDK target and forwards here. + package func makeContainer(type: T.Type, options: BaseContainerOptions? = nil) -> Container? { + if options != nil { + // Set options + } + + if T.self == CollectContainer.self { + Log.info(message: .COLLECT_CONTAINER_CREATED, contextOptions: self.contextOptions) + return Container(skyflow: self) + } + + if T.self == RevealContainer.self { + Log.info(message: .REVEAL_CONTAINER_CREATED, contextOptions: self.contextOptions) + return Container(skyflow: self) + } + if T.self == ComposableContainer.self { + return Container(skyflow: self, options: options) + } + return nil + } + + package func callRevealOnFailure(callback: Callback, errorObject: Error) { + let result = ["errors": [errorObject]] + callback.onFailure(result) + } +} + +package class LogCallback: Callback { + package var clientCallback: Callback + package var contextOptions: ContextOptions + package var onSuccessHandler: () -> Void + package var onFailureHandler: () -> Void + + package init(clientCallback: Callback, contextOptions: ContextOptions, onSuccessHandler: @escaping () -> Void, onFailureHandler: @escaping () -> Void) { + self.clientCallback = clientCallback + self.contextOptions = contextOptions + self.onSuccessHandler = onSuccessHandler + self.onFailureHandler = onFailureHandler + } + + package func onSuccess(_ responseBody: Any) { + self.onSuccessHandler() + clientCallback.onSuccess(responseBody) + } + + package func onFailure(_ error: Any) { + self.onFailureHandler() + clientCallback.onFailure(error) + } +} diff --git a/Sources/SkyflowCore/core/ContextOptions.swift b/Sources/SkyflowCore/core/ContextOptions.swift new file mode 100644 index 00000000..91e515aa --- /dev/null +++ b/Sources/SkyflowCore/core/ContextOptions.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// +// File.swift +// +// +// Created by Akhil Anil Mangala on 07/10/21. +// + +import Foundation + +package struct ContextOptions { + package var logLevel: LogLevel + package var env: Env + package var interface: InterfaceName + + package init(logLevel: LogLevel = .ERROR, env: Env = .PROD, interface: InterfaceName = .EMPTY) { + self.logLevel = logLevel + self.env = env + self.interface = interface + } +} diff --git a/Sources/Skyflow/core/Element.swift b/Sources/SkyflowCore/core/Element.swift similarity index 100% rename from Sources/Skyflow/core/Element.swift rename to Sources/SkyflowCore/core/Element.swift diff --git a/Sources/Skyflow/core/Env.swift b/Sources/SkyflowCore/core/Env.swift similarity index 100% rename from Sources/Skyflow/core/Env.swift rename to Sources/SkyflowCore/core/Env.swift diff --git a/Sources/Skyflow/core/EventName.swift b/Sources/SkyflowCore/core/EventName.swift similarity index 100% rename from Sources/Skyflow/core/EventName.swift rename to Sources/SkyflowCore/core/EventName.swift diff --git a/Sources/Skyflow/core/InterfaceName.swift b/Sources/SkyflowCore/core/InterfaceName.swift similarity index 95% rename from Sources/Skyflow/core/InterfaceName.swift rename to Sources/SkyflowCore/core/InterfaceName.swift index 54534af7..d2f20748 100644 --- a/Sources/Skyflow/core/InterfaceName.swift +++ b/Sources/SkyflowCore/core/InterfaceName.swift @@ -4,14 +4,14 @@ // // File.swift -// +// // // Created by Akhil Anil Mangala on 29/11/21. // import Foundation -internal enum InterfaceName { +package enum InterfaceName { case COLLECT_CONTAINER case REVEAL_CONTAINER case COMPOSABLE_CONTAINER @@ -21,7 +21,7 @@ internal enum InterfaceName { case GETBYID case GET case EMPTY - + var description: String { switch self { case .COLLECT_CONTAINER: return "collect container" diff --git a/Sources/Skyflow/core/Log.swift b/Sources/SkyflowCore/core/Log.swift similarity index 66% rename from Sources/Skyflow/core/Log.swift rename to Sources/SkyflowCore/core/Log.swift index feb47db1..ad8b6e12 100644 --- a/Sources/Skyflow/core/Log.swift +++ b/Sources/SkyflowCore/core/Log.swift @@ -6,23 +6,23 @@ import Foundation -internal class Log { - internal static func debug(message: Message, values: [String] = [], contextOptions: ContextOptions) { +package class Log { + package static func debug(message: Message, values: [String] = [], contextOptions: ContextOptions) { if contextOptions.logLevel.rawValue < 1 { print("DEBUG: [Skyflow] Interface: \(contextOptions.interface.description) - \(message.getDescription(values: values))") } } - internal static func info(message: Message, values: [String] = [], contextOptions: ContextOptions) { + package static func info(message: Message, values: [String] = [], contextOptions: ContextOptions) { if contextOptions.logLevel.rawValue < 2 { print("INFO: [Skyflow] Interface: \(contextOptions.interface.description) - \(message.getDescription(values: values))") } } - internal static func warn(message: Message, values: [String] = [], contextOptions: ContextOptions) { + package static func warn(message: Message, values: [String] = [], contextOptions: ContextOptions) { if contextOptions.logLevel.rawValue < 3 { print("WARN: [Skyflow] Interface: \(contextOptions.interface.description) - \(message.getDescription(values: values))") } } - internal static func error(message: String, values: [String] = [], contextOptions: ContextOptions) { + package static func error(message: String, values: [String] = [], contextOptions: ContextOptions) { print("ERROR: [Skyflow]: \(message)") } } diff --git a/Sources/Skyflow/core/LogLevel.swift b/Sources/SkyflowCore/core/LogLevel.swift similarity index 100% rename from Sources/Skyflow/core/LogLevel.swift rename to Sources/SkyflowCore/core/LogLevel.swift diff --git a/Sources/Skyflow/core/Message.swift b/Sources/SkyflowCore/core/Message.swift similarity index 99% rename from Sources/Skyflow/core/Message.swift rename to Sources/SkyflowCore/core/Message.swift index e7c7b918..06eb2c9f 100644 --- a/Sources/Skyflow/core/Message.swift +++ b/Sources/SkyflowCore/core/Message.swift @@ -11,7 +11,7 @@ import Foundation -internal enum Message { +package enum Message { case CLIENT_INITIALIZED case COLLECT_CONTAINER_CREATED case REVEAL_CONTAINER_CREATED @@ -76,7 +76,7 @@ internal enum Message { case .INSERT_TRIGGERED: return "Insert method triggered." case .DETOKENIZE_TRIGGERED: return "Detokenize method triggered." case .GET_BY_ID_TRIGGERED: return "Get by ID triggered." - + //Used in tests case .CLIENT_CONNECTION: return "client connection not established" // A case .CANNOT_CHANGE_ELEMENT: return "Element can't be changed" // A diff --git a/Sources/Skyflow/core/Options.swift b/Sources/SkyflowCore/core/Options.swift similarity index 100% rename from Sources/Skyflow/core/Options.swift rename to Sources/SkyflowCore/core/Options.swift diff --git a/Sources/SkyflowCore/core/container/BaseContainerOptions.swift b/Sources/SkyflowCore/core/container/BaseContainerOptions.swift new file mode 100644 index 00000000..82239f81 --- /dev/null +++ b/Sources/SkyflowCore/core/container/BaseContainerOptions.swift @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared storage for the container options (composable layout/styles). Each +// SDK product defines its own public `BaseContainerOptions` struct exposing that +// contract's initializers (identical in both contracts today). + +import Foundation + +package struct BaseContainerOptions { + package var layout: [Int] + package var styles: Styles? + package var errorTextStyles: Styles? + + package init(){ + layout = [0] + } + + package init(layout: [Int], styles: Styles? = Styles(), errorTextStyles: Styles? = Styles()) { + self.layout = layout + self.styles = styles + self.errorTextStyles = errorTextStyles + } + +} diff --git a/Sources/SkyflowCore/core/container/Container.swift b/Sources/SkyflowCore/core/container/Container.swift new file mode 100644 index 00000000..c0302169 --- /dev/null +++ b/Sources/SkyflowCore/core/container/Container.swift @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// +// File.swift +// +// +// Created by Akhil Anil Mangala on 27/07/21. +// + +import Foundation + +public class Container { + package var skyflow: ClientBase + package var elements: [TextField] = [] + package var revealElements: [Label] = [] + package var containerOptions: BaseContainerOptions? = nil + + package init(skyflow: ClientBase) { + self.skyflow = skyflow + } + package init(skyflow: ClientBase, options: BaseContainerOptions? = nil){ + self.containerOptions = options + self.skyflow = skyflow + } +} diff --git a/Sources/Skyflow/core/container/ContainerProtocol.swift b/Sources/SkyflowCore/core/container/ContainerProtocol.swift similarity index 100% rename from Sources/Skyflow/core/container/ContainerProtocol.swift rename to Sources/SkyflowCore/core/container/ContainerProtocol.swift diff --git a/Sources/Skyflow/core/container/ContainerType.swift b/Sources/SkyflowCore/core/container/ContainerType.swift similarity index 100% rename from Sources/Skyflow/core/container/ContainerType.swift rename to Sources/SkyflowCore/core/container/ContainerType.swift diff --git a/Sources/Skyflow/core/protocols/Callback.swift b/Sources/SkyflowCore/core/protocols/Callback.swift similarity index 100% rename from Sources/Skyflow/core/protocols/Callback.swift rename to Sources/SkyflowCore/core/protocols/Callback.swift diff --git a/Sources/Skyflow/core/protocols/TokenProvider.swift b/Sources/SkyflowCore/core/protocols/TokenProvider.swift similarity index 100% rename from Sources/Skyflow/core/protocols/TokenProvider.swift rename to Sources/SkyflowCore/core/protocols/TokenProvider.swift diff --git a/Sources/Skyflow/elements/FormatTextField.swift b/Sources/SkyflowCore/elements/FormatTextField.swift similarity index 97% rename from Sources/Skyflow/elements/FormatTextField.swift rename to Sources/SkyflowCore/elements/FormatTextField.swift index 0337357a..12c9a170 100644 --- a/Sources/Skyflow/elements/FormatTextField.swift +++ b/Sources/SkyflowCore/elements/FormatTextField.swift @@ -7,7 +7,7 @@ import UIKit #endif /// textfield used in SkyflowTextField -internal class FormatTextField: UITextField { +package class FormatTextField: UITextField { enum FormatPatternChar: String, CaseIterable { case lettersAndDigit = "*" case anyLetter = "@" @@ -37,7 +37,7 @@ internal class FormatTextField: UITextField { set { } } - override func leftViewRect(forBounds bounds: CGRect) -> CGRect { + package override func leftViewRect(forBounds bounds: CGRect) -> CGRect { var textRect = super.leftViewRect(forBounds: bounds) textRect.origin.x += 2 return textRect @@ -49,7 +49,7 @@ internal class FormatTextField: UITextField { is applied programmatically by calling formatText */ @available(*, deprecated, message: "Don't use this method.") - override var text: String? { + package override var text: String? { set { secureText = newValue } @@ -240,7 +240,7 @@ internal class FormatTextField: UITextField { } extension FormatTextField { - override var description: String { + package override var description: String { return NSStringFromClass(self.classForCoder) } } diff --git a/Sources/Skyflow/elements/PaddingLabel.swift b/Sources/SkyflowCore/elements/PaddingLabel.swift similarity index 78% rename from Sources/Skyflow/elements/PaddingLabel.swift rename to Sources/SkyflowCore/elements/PaddingLabel.swift index 015be7ac..df1bb695 100644 --- a/Sources/Skyflow/elements/PaddingLabel.swift +++ b/Sources/SkyflowCore/elements/PaddingLabel.swift @@ -8,20 +8,20 @@ import Foundation import UIKit -internal class PaddingLabel: UILabel { +package class PaddingLabel: UILabel { internal var insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - override func drawText(in rect: CGRect) { + package override func drawText(in rect: CGRect) { super.drawText(in: rect.inset(by: insets)) } - override var intrinsicContentSize: CGSize { + package override var intrinsicContentSize: CGSize { let size = super.intrinsicContentSize return CGSize(width: size.width + insets.left + insets.right, height: size.height + insets.top + insets.bottom) } - override var bounds: CGRect { + package override var bounds: CGRect { didSet { // ensures this works within stack views if multi-line preferredMaxLayoutWidth = bounds.width - (insets.left + insets.right) diff --git a/Sources/Skyflow/elements/SkyflowElement.swift b/Sources/SkyflowCore/elements/SkyflowElement.swift similarity index 80% rename from Sources/Skyflow/elements/SkyflowElement.swift rename to Sources/SkyflowCore/elements/SkyflowElement.swift index 0ac78fef..834feb93 100644 --- a/Sources/Skyflow/elements/SkyflowElement.swift +++ b/Sources/SkyflowCore/elements/SkyflowElement.swift @@ -13,23 +13,23 @@ import UIKit public class SkyflowElement: UIView { internal var isRequired = false - internal var fieldType: ElementType! - internal var columnName: String! - internal var tableName: String? - internal var skyflowID: String? + package var fieldType: ElementType! + package var columnName: String! + package var tableName: String? + package var skyflowId: String? internal var horizontalConstraints = [NSLayoutConstraint]() internal var verticalConstraint = [NSLayoutConstraint]() - internal var collectInput: CollectElementInput! - internal var options: CollectElementOptions! + package var collectInput: BaseCollectElementInput! + internal var options: BaseCollectElementOptions! internal var contextOptions: ContextOptions! - internal var elements: [TextField] = [] + package var elements: [TextField] = [] /// Describes `SkyflowElement` input State` - internal var state: State { + package var state: State { return State(columnName: self.columnName, isRequired: self.isRequired) } - internal func getState() -> [String: Any] { + package func getState() -> [String: Any] { return state.getState() } @@ -38,7 +38,7 @@ public class SkyflowElement: UIView { initialization() } - internal init(input: CollectElementInput, options: CollectElementOptions, contextOptions: ContextOptions, elements: [TextField]) { + internal init(input: BaseCollectElementInput, options: BaseCollectElementOptions, contextOptions: ContextOptions, elements: [TextField]) { super.init(frame: CGRect()) self.elements = elements collectInput = input @@ -58,13 +58,13 @@ public class SkyflowElement: UIView { NotificationCenter.default.removeObserver(self) } - /// Field Configuration + /// Field BaseConfiguration internal func setupField() { - tableName = collectInput.table + tableName = collectInput.tableName columnName = collectInput.column fieldType = collectInput.type isRequired = options.required - skyflowID = collectInput.skyflowID + skyflowId = collectInput.skyflowId } internal func getOutput() -> String? { diff --git a/Sources/Skyflow/elements/TextField.swift b/Sources/SkyflowCore/elements/TextField.swift similarity index 96% rename from Sources/Skyflow/elements/TextField.swift rename to Sources/SkyflowCore/elements/TextField.swift index 4401b4bf..843257b7 100644 --- a/Sources/Skyflow/elements/TextField.swift +++ b/Sources/SkyflowCore/elements/TextField.swift @@ -19,11 +19,11 @@ public class TextField: SkyflowElement, Element, BaseElement { var onBeginEditing: (() -> Void)? var onEndEditing: (() -> Void)? var onFocusIsTrue: (() -> Void)? - internal var textField = FormatTextField(frame: .zero) - internal var errorMessage = PaddingLabel(frame: .zero) + package var textField = FormatTextField(frame: .zero) + package var errorMessage = PaddingLabel(frame: .zero) internal var isDirty = false internal var validationRules = ValidationSet() - internal var userValidationRules = ValidationSet() + package var userValidationRules = ValidationSet() internal var stackView = UIStackView() internal var textFieldLabel = PaddingLabel(frame: .zero) internal var hasBecomeResponder: Bool = false @@ -45,7 +45,7 @@ public class TextField: SkyflowElement, Element, BaseElement { internal var dropdownButton = UIButton() internal var selectedCardBrand: CardType? = nil - internal var uuid: String = "" + package var uuid: String = "" internal var textFieldCornerRadius: CGFloat { get { @@ -82,11 +82,11 @@ public class TextField: SkyflowElement, Element, BaseElement { didSet { setMainPaddings() } } - internal override var state: State { + package override var state: State { return StateforText(tf: self) } - override init(input: CollectElementInput, options: CollectElementOptions, contextOptions: ContextOptions, elements: [TextField]? = nil) { + override init(input: BaseCollectElementInput, options: BaseCollectElementOptions, contextOptions: ContextOptions, elements: [TextField]? = nil) { super.init(input: input, options: options, contextOptions: contextOptions, elements: elements ?? []) self.userValidationRules.append(input.validations) self.textFieldDelegate = TextFieldValidationDelegate(collectField: self) @@ -166,7 +166,7 @@ public class TextField: SkyflowElement, Element, BaseElement { } - internal func isMounted() -> Bool { + package func isMounted() -> Bool { var flag = false if Thread.isMainThread { flag = self.window != nil @@ -185,7 +185,7 @@ public class TextField: SkyflowElement, Element, BaseElement { internal var onBlurHandler: (([String: Any]) -> Void)? internal var onReadyHandler: (([String: Any]) -> Void)? internal var onFocusHandler: (([String: Any]) -> Void)? - internal var onSubmitHandler: (() -> Void)? + package var onSubmitHandler: (() -> Void)? override func getOutput() -> String? { @@ -194,7 +194,7 @@ public class TextField: SkyflowElement, Element, BaseElement { internal var actualValue: String = "" - internal func getValue() -> String { + package func getValue() -> String { return actualValue } @@ -203,13 +203,15 @@ public class TextField: SkyflowElement, Element, BaseElement { return textField.getSecureRawText } - public func update(update: CollectElementInput){ + // The public update(update:) taking each SDK's CollectElementInput struct + // is defined per SDK target and forwards here. + package func update(data update: BaseCollectElementInput){ collectInput.placeholder = update.placeholder if update.column.isEmpty != true { collectInput.column = update.column } - if update.table.isEmpty != true { - collectInput.table = update.table + if update.tableName.isEmpty != true { + collectInput.tableName = update.tableName } if update.column.isEmpty != true { collectInput.column = update.column @@ -250,11 +252,13 @@ public class TextField: SkyflowElement, Element, BaseElement { setupField() } - public func update(updateOptions: CollectElementOptions){ + // The public update(updateOptions:) taking each SDK's CollectElementOptions + // struct is defined per SDK target and forwards here. + package func update(optionsData updateOptions: BaseCollectElementOptions){ if(updateOptions.cardMetaData != nil && self.fieldType == .CARD_NUMBER){ self.options.cardMetaData = updateOptions.cardMetaData - if let schemes = self.options.cardMetaData?["scheme"] as? [Skyflow.CardType] { + if let schemes = self.options.cardMetaData?["scheme"] as? [CardType] { if schemes.isEmpty { selectedCardBrand = nil listCardTypes = nil @@ -417,7 +421,7 @@ public class TextField: SkyflowElement, Element, BaseElement { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var image = UIImage(named: "Unknown-Card", in: resourceBundle, compatibleWith: nil) #endif @@ -478,7 +482,7 @@ public class TextField: SkyflowElement, Element, BaseElement { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var image = UIImage(named: "Copy-Icon", in: resourceBundle, compatibleWith: nil) #endif @@ -503,7 +507,7 @@ public class TextField: SkyflowElement, Element, BaseElement { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var image = UIImage(named: "Success-Icon", in: resourceBundle, compatibleWith: nil) #endif @@ -516,7 +520,7 @@ public class TextField: SkyflowElement, Element, BaseElement { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var copyImage = UIImage(named: "Copy-Icon", in: resourceBundle, compatibleWith: nil) #endif @@ -541,7 +545,7 @@ public class TextField: SkyflowElement, Element, BaseElement { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) let image = UIImage(named: name, in: resourceBundle, compatibleWith: nil) #endif @@ -626,7 +630,7 @@ public class TextField: SkyflowElement, Element, BaseElement { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) dropdownButton.setImage(UIImage(named: "dropdown", in: resourceBundle, compatibleWith: nil), for: .normal) #endif @@ -883,7 +887,7 @@ extension TextField { } } - func updateErrorMessage() { + package func updateErrorMessage() { var isRequiredCheckFailed = false diff --git a/Sources/Skyflow/elements/TextFieldValidationDelegate.swift b/Sources/SkyflowCore/elements/TextFieldValidationDelegate.swift similarity index 100% rename from Sources/Skyflow/elements/TextFieldValidationDelegate.swift rename to Sources/SkyflowCore/elements/TextFieldValidationDelegate.swift diff --git a/Sources/SkyflowCore/elements/core/BaseCollectElementInput.swift b/Sources/SkyflowCore/elements/core/BaseCollectElementInput.swift new file mode 100644 index 00000000..f18dec77 --- /dev/null +++ b/Sources/SkyflowCore/elements/core/BaseCollectElementInput.swift @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared storage for the collect element input. Each SDK product defines its +// own public `CollectElementInput` struct exposing that contract's +// initializers (the legacy SDK uses `table:`/`skyflowID:` argument labels, +// FlowVault uses `tableName:`/`skyflowId:`); those structs wrap this value and +// hand it to the core element factories. Value semantics: every element stores +// its own copy, so post-creation mutations never leak between elements. + +import Foundation + +package struct BaseCollectElementInput { + package var tableName: String + package var column: String + package var inputStyles: Styles + package var labelStyles: Styles + package var errorTextStyles: Styles + package var iconStyles: Styles + package var label: String + package var placeholder: String + package var type: ElementType? + package var validations: ValidationSet + package var skyflowId: String? + + package init(tableName: String, column: String, inputStyles: Styles, labelStyles: Styles, errorTextStyles: Styles, iconStyles: Styles, label: String, placeholder: String, type: ElementType?, validations: ValidationSet, skyflowId: String?) { + self.tableName = tableName + self.column = column + self.inputStyles = inputStyles + self.labelStyles = labelStyles + self.errorTextStyles = errorTextStyles + self.iconStyles = iconStyles + self.label = label + self.placeholder = placeholder + self.type = type + self.validations = validations + self.skyflowId = skyflowId + } +} diff --git a/Sources/Skyflow/elements/core/CollectElementOptions.swift b/Sources/SkyflowCore/elements/core/BaseCollectElementOptions.swift similarity index 54% rename from Sources/Skyflow/elements/core/CollectElementOptions.swift rename to Sources/SkyflowCore/elements/core/BaseCollectElementOptions.swift index d886b004..2bbd5100 100644 --- a/Sources/Skyflow/elements/core/CollectElementOptions.swift +++ b/Sources/SkyflowCore/elements/core/BaseCollectElementOptions.swift @@ -2,32 +2,34 @@ * Copyright (c) 2022 Skyflow */ -// An Object that describes Options parameter for SkyflowInputField +// Shared storage for the collect element options. Each SDK product defines its +// own public `CollectElementOptions` struct exposing that contract's +// initializer (identical in both contracts today); those structs wrap this +// value and hand it to the core element factories. import Foundation #if os(iOS) import UIKit #endif -public struct CollectElementOptions { - var required: Bool - var enableCardIcon: Bool - var format: String - var translation: [ Character: String ]? - var enableCopy: Bool - var cardMetaData: [ String: Any]? +package struct BaseCollectElementOptions { + package var required: Bool + package var enableCardIcon: Bool + package var format: String + package var translation: [ Character: String ]? + package var enableCopy: Bool + package var cardMetaData: [ String: Any]? - - public init(required: Bool? = false, enableCardIcon: Bool = true, format: String = "mm/yy", translation: [ Character: String ]? = nil, enableCopy: Bool = false, cardMetaData: [ String: Any]? = nil) { - self.required = required! + package init(required: Bool = false, enableCardIcon: Bool = true, format: String = "mm/yy", translation: [ Character: String ]? = nil, enableCopy: Bool = false, cardMetaData: [ String: Any]? = nil) { + self.required = required self.enableCardIcon = enableCardIcon self.format = format self.translation = translation self.enableCopy = enableCopy self.cardMetaData = cardMetaData - + if (self.translation != nil){ - for (key, value) in self.translation! { + for (_, value) in self.translation! { if value == "" { var contextOptions = ContextOptions() contextOptions.interface = InterfaceName.COLLECT_CONTAINER @@ -40,4 +42,3 @@ public struct CollectElementOptions { } } - diff --git a/Sources/Skyflow/elements/core/ElementType.swift b/Sources/SkyflowCore/elements/core/ElementType.swift similarity index 96% rename from Sources/Skyflow/elements/core/ElementType.swift rename to Sources/SkyflowCore/elements/core/ElementType.swift index 87f0d820..88b1725f 100644 --- a/Sources/Skyflow/elements/core/ElementType.swift +++ b/Sources/SkyflowCore/elements/core/ElementType.swift @@ -37,7 +37,7 @@ public enum ElementType: Int, CaseIterable { /// Field type that requires Credit Card Number input formatting and validation. case CARD_NUMBER - /// Field type that requires Card Expiration Date input formatting and validation, format can be set through CollectElementOptions, defaul is MM/YY + /// Field type that requires Card Expiration Date input formatting and validation, format can be set through BaseCollectElementOptions, defaul is MM/YY case EXPIRATION_DATE /// Field type that requires Card CVV input formatting and validation. @@ -52,7 +52,7 @@ public enum ElementType: Int, CaseIterable { /// Field type that requires Card Expiration Month formatting and validation (format: MM) case EXPIRATION_MONTH - /// Field type that requires Card Expiration Year formatting and validation, format can be set through CollectElementOptions for YY, defaul is YYYY + /// Field type that requires Card Expiration Year formatting and validation, format can be set through BaseCollectElementOptions for YY, defaul is YYYY case EXPIRATION_YEAR diff --git a/Sources/Skyflow/elements/core/state/State.swift b/Sources/SkyflowCore/elements/core/state/State.swift similarity index 91% rename from Sources/Skyflow/elements/core/state/State.swift rename to Sources/SkyflowCore/elements/core/state/State.swift index 706913c3..a9a6d0d6 100644 --- a/Sources/Skyflow/elements/core/state/State.swift +++ b/Sources/SkyflowCore/elements/core/state/State.swift @@ -10,8 +10,8 @@ import UIKit /// An object that describes `SkyflowTextField` state. /// State attributes are read-only. -internal class State { - /// `CollectElementOptions.columnName` associated with `SkyflowTextField` +package class State { + /// `BaseCollectElementOptions.columnName` associated with `SkyflowTextField` internal(set) open var columnName: String! /// set as true if `SkyflowTextField` input is required to fill diff --git a/Sources/Skyflow/elements/core/state/StateforText.swift b/Sources/SkyflowCore/elements/core/state/StateforText.swift similarity index 98% rename from Sources/Skyflow/elements/core/state/StateforText.swift rename to Sources/SkyflowCore/elements/core/state/StateforText.swift index 75782683..9b019c07 100644 --- a/Sources/Skyflow/elements/core/state/StateforText.swift +++ b/Sources/SkyflowCore/elements/core/state/StateforText.swift @@ -7,7 +7,7 @@ import Foundation import UIKit #endif -internal class StateforText: State +package class StateforText: State { /// true if `SkyflowTextField` input in valid internal(set) open var isValid = false diff --git a/Sources/Skyflow/elements/core/styles/Style.swift b/Sources/SkyflowCore/elements/core/styles/Style.swift similarity index 100% rename from Sources/Skyflow/elements/core/styles/Style.swift rename to Sources/SkyflowCore/elements/core/styles/Style.swift diff --git a/Sources/Skyflow/elements/core/styles/Styles.swift b/Sources/SkyflowCore/elements/core/styles/Styles.swift similarity index 100% rename from Sources/Skyflow/elements/core/styles/Styles.swift rename to Sources/SkyflowCore/elements/core/styles/Styles.swift diff --git a/Sources/Skyflow/elements/utils/CardBin.swift b/Sources/SkyflowCore/elements/utils/CardBin.swift similarity index 100% rename from Sources/Skyflow/elements/utils/CardBin.swift rename to Sources/SkyflowCore/elements/utils/CardBin.swift diff --git a/Sources/Skyflow/elements/utils/CardType.swift b/Sources/SkyflowCore/elements/utils/CardType.swift similarity index 100% rename from Sources/Skyflow/elements/utils/CardType.swift rename to Sources/SkyflowCore/elements/utils/CardType.swift diff --git a/Sources/Skyflow/elements/utils/CharacterSet.swift b/Sources/SkyflowCore/elements/utils/CharacterSet.swift similarity index 100% rename from Sources/Skyflow/elements/utils/CharacterSet.swift rename to Sources/SkyflowCore/elements/utils/CharacterSet.swift diff --git a/Sources/Skyflow/elements/validations/ElementValueMatchRule.swift b/Sources/SkyflowCore/elements/validations/ElementValueMatchRule.swift similarity index 100% rename from Sources/Skyflow/elements/validations/ElementValueMatchRule.swift rename to Sources/SkyflowCore/elements/validations/ElementValueMatchRule.swift diff --git a/Sources/Skyflow/elements/validations/LengthMatchRule.swift b/Sources/SkyflowCore/elements/validations/LengthMatchRule.swift similarity index 100% rename from Sources/Skyflow/elements/validations/LengthMatchRule.swift rename to Sources/SkyflowCore/elements/validations/LengthMatchRule.swift diff --git a/Sources/Skyflow/elements/validations/RegexMatchRule.swift b/Sources/SkyflowCore/elements/validations/RegexMatchRule.swift similarity index 100% rename from Sources/Skyflow/elements/validations/RegexMatchRule.swift rename to Sources/SkyflowCore/elements/validations/RegexMatchRule.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowInternalValidatorProtocol.swift b/Sources/SkyflowCore/elements/validations/SkyflowInternalValidatorProtocol.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowInternalValidatorProtocol.swift rename to Sources/SkyflowCore/elements/validations/SkyflowInternalValidatorProtocol.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidateCardExpirationDate.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidateCardExpirationDate.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidateCardExpirationDate.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidateCardExpirationDate.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidateCardNumber.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidateCardNumber.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidateCardNumber.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidateCardNumber.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidateExpirationMonth.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationMonth.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidateExpirationMonth.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationMonth.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidateExpirationYear.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationYear.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidateExpirationYear.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationYear.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidateLengthMatch.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidateLengthMatch.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidateLengthMatch.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidateLengthMatch.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidationError.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidationError.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidationError.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidationError.swift diff --git a/Sources/Skyflow/elements/validations/SkyflowValidator.swift b/Sources/SkyflowCore/elements/validations/SkyflowValidator.swift similarity index 100% rename from Sources/Skyflow/elements/validations/SkyflowValidator.swift rename to Sources/SkyflowCore/elements/validations/SkyflowValidator.swift diff --git a/Sources/Skyflow/elements/validations/ValidationRule.swift b/Sources/SkyflowCore/elements/validations/ValidationRule.swift similarity index 100% rename from Sources/Skyflow/elements/validations/ValidationRule.swift rename to Sources/SkyflowCore/elements/validations/ValidationRule.swift diff --git a/Sources/Skyflow/elements/validations/ValidationSet.swift b/Sources/SkyflowCore/elements/validations/ValidationSet.swift similarity index 91% rename from Sources/Skyflow/elements/validations/ValidationSet.swift rename to Sources/SkyflowCore/elements/validations/ValidationSet.swift index 3a0a8fb9..84fc314d 100644 --- a/Sources/Skyflow/elements/validations/ValidationSet.swift +++ b/Sources/SkyflowCore/elements/validations/ValidationSet.swift @@ -8,7 +8,7 @@ import Foundation public struct ValidationSet { - internal var rules = [ValidationRule]() + package var rules = [ValidationRule]() public init() { } diff --git a/Sources/Skyflow/errors/ErrorCodes.swift b/Sources/SkyflowCore/errors/ErrorCodes.swift similarity index 57% rename from Sources/Skyflow/errors/ErrorCodes.swift rename to Sources/SkyflowCore/errors/ErrorCodes.swift index d117f4bc..d868c185 100644 --- a/Sources/Skyflow/errors/ErrorCodes.swift +++ b/Sources/SkyflowCore/errors/ErrorCodes.swift @@ -6,7 +6,7 @@ import Foundation -internal enum ErrorCodes: CustomStringConvertible { +package enum ErrorCodes: CustomStringConvertible { // No message values case EMPTY_TABLE_NAME(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid type for 'table' key value in collect element. Specify a value of type string instead.") case EMPTY_COMPOSABLE_LAYOUT_ARRAY(code: Int = 400, message: String = "\(LangAndVersion) Mount failed. Layout array is empty in composable container options. Specify a valid layout array.") @@ -17,6 +17,8 @@ internal enum ErrorCodes: CustomStringConvertible { case EMPTY_VAULT_URL(code: Int = 400, message: String = "\(LangAndVersion) Initialization failed. Invalid credentials. Specify a valid 'vaultURL'.") case EMPTY_IDS(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'ids' key cannot be an array of empty strings in records at index . Specify non-empty values instead.", value: String) case EMPTY_ID_VALUE(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'id' cannot be empty in 'ids' array in 'records' at index . Specify non-empty values instead.", value: String) + case EMPTY_SKYFLOW_ID(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'skyflowId' cannot be an empty string for . Provide a valid 'skyflowId' or remove it.", value: String) + case INVALID_TOKEN_GROUP_REDACTION_ENTRY(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'tokenGroupRedactions' entry at index . 'tokenGroupName' and 'redaction' must be non-empty strings.", value: String) case RECORDS_KEY_ERROR(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Missing 'records' key. Provide a valid 'records' key.") case TABLE_KEY_ERROR(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Missing 'table' key in records at index . Provide a valid 'table' key.", value: String) case FIELDS_KEY_ERROR(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Missing 'fields' key in records at index . Provide a valid 'fields' key.", value: String) @@ -30,7 +32,6 @@ internal enum ErrorCodes: CustomStringConvertible { case INVALID_FIELDS_TYPE(code: Int = 400, message: String = "\(LangAndVersion) Validation error.invaid 'fields' key value in record at index . Specify a value of type array for 'fields' key.", value: String) case INVALID_RECORDS_TYPE(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'records' key found. Specify a value of type array instead.") case INVALID_BEARER_TOKEN_FORMAT(code: Int = 400, message: String = "\(LangAndVersion) Token generated from 'getBearerToken' callback function is invalid. Make sure the implementation of 'getBearerToken' is correct.") - case MISSING_RECORDS_ARRAY(code: Int = 404, message: String = "\(LangAndVersion) Validation error.'records' key not found in additionalFields. Specify a 'records' key in addtionalFields.") case MISSING_RECORDS_IN_ADDITIONAL_FIELDS(code: Int = 404, message: String = "\(LangAndVersion) Validation error.'records' object cannot be empty within additionalFields. Specify a non-empty value instead.") case EMPTY_RECORDS_OBJECT(code: Int = 404, message: String = "\(LangAndVersion) Validation error. 'records' key cannot be empty. Provide a non-empty value instead.") case MISSING_RECORDS_IN_GETBYID(code: Int = 404, message: String = "\(LangAndVersion) Validation error. 'records' key cannot be empty. Provide a non-empty value instead.") @@ -69,43 +70,44 @@ internal enum ErrorCodes: CustomStringConvertible { case UPSERT_OPTION_CANNOT_BE_EMPTY(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'upsert' key cannot be an empty array in insert options. Make sure to add atleast one table column object in upsert array.") case MISSING_COLUMN_NAME_IN_USERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Missing 'column' key in upsert array at index . Provide a valid 'column' key.", value: String) case COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'table' key in upsert array at index . Specify a value of type string instead.", value: String) + case UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'uniqueColumns' cannot be empty in upsert option at index . Specify at least one column.", value: String) case TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'table' key in upsert array at index . Specify a value of type string instead.", value: String) case MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(code: Int = 400, message: String = "\(LangAndVersion) Mount failed. Invalid layout array values. Make sure all values in the layout array are positive numbers.") var code: Int { switch self { // No Formatting required // swiftlint:disable:next line_length - case .EMPTY_TABLE_NAME(let code, _), .EMPTY_VAULT_ID(let code, _), .EMPTY_VAULT_URL(let code, _),.RECORDS_KEY_ERROR( let code, _),.EMPTY_TOKEN_ID(let code, _), .ID_KEY_ERROR(let code, _), .INVALID_RECORDS_TYPE(let code, _), .EMPTY_COLUMN_NAME(let code, _), .INVALID_BEARER_TOKEN_FORMAT(let code, _), .MISSING_RECORDS_ARRAY(let code, _), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS(let code, _), .EMPTY_RECORDS_OBJECT(let code, _), .MISSING_RECORDS_IN_GETBYID(let code, _),.INVALID_URL(let code, _), .INVALID_IDS_TYPE(let code, _),.REDACTION_WITH_TOKEN_NOT_SUPPORTED(let code, _), .TOKENS_GET_COLUMN_NOT_SUPPPORTED(let code, _), .MISSING_COLUMN_NAME(let code,_), .UPSERT_OPTION_CANNOT_BE_EMPTY(let code, _),.INVALID_REDACTION_TYPE(let code, _),.EMPTY_COLUMN_NAME_IN_COLLECT(let code, _), .EMPTY_TABLE_NAME_IN_COLLECT(let code, _), .REGEX_MATCH_FAILED(let code, _),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED(let code, _),.MISSING_IDS_OR_COLUMN_VALUES_IN_GET(let code, _), .MISSING_RECORD_COLUMN_VALUE(let code, _), .INVALID_COLUMN_VALUES_IN_GET(let code, _), .EMPTY_COMPOSABLE_LAYOUT_ARRAY(let code, _), .MISSING_COMPOSABLE_LAYOUT_KEY(let code, _), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(let code, _),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: let code, message: _): + case .EMPTY_TABLE_NAME(let code, _), .EMPTY_VAULT_ID(let code, _), .EMPTY_VAULT_URL(let code, _),.RECORDS_KEY_ERROR( let code, _),.EMPTY_TOKEN_ID(let code, _), .ID_KEY_ERROR(let code, _), .INVALID_RECORDS_TYPE(let code, _), .EMPTY_COLUMN_NAME(let code, _), .INVALID_BEARER_TOKEN_FORMAT(let code, _), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS(let code, _), .EMPTY_RECORDS_OBJECT(let code, _), .MISSING_RECORDS_IN_GETBYID(let code, _),.INVALID_URL(let code, _), .INVALID_IDS_TYPE(let code, _),.REDACTION_WITH_TOKEN_NOT_SUPPORTED(let code, _), .TOKENS_GET_COLUMN_NOT_SUPPPORTED(let code, _), .MISSING_COLUMN_NAME(let code,_), .UPSERT_OPTION_CANNOT_BE_EMPTY(let code, _),.INVALID_REDACTION_TYPE(let code, _),.EMPTY_COLUMN_NAME_IN_COLLECT(let code, _), .EMPTY_TABLE_NAME_IN_COLLECT(let code, _), .REGEX_MATCH_FAILED(let code, _),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED(let code, _),.MISSING_IDS_OR_COLUMN_VALUES_IN_GET(let code, _), .MISSING_RECORD_COLUMN_VALUE(let code, _), .INVALID_COLUMN_VALUES_IN_GET(let code, _), .EMPTY_COMPOSABLE_LAYOUT_ARRAY(let code, _), .MISSING_COMPOSABLE_LAYOUT_KEY(let code, _), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(let code, _),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: let code, message: _): return code // Single value formatting // swiftlint:disable:next line_length // Multi value formatting - case .EMPTY_IDS(let code, _, _), .EMPTY_ID_VALUE(let code, _, _), .TABLE_KEY_ERROR(let code, _, _), .FIELDS_KEY_ERROR(let code, _, _), .EMPTY_FIELDS_KEY(let code, _, _), .REDACTION_KEY_ERROR(let code, _, _), .MISSING_KEY_IDS(let code, _, _), .INVALID_TABLE_NAME_TYPE(let code, _, _), .INVALID_FIELDS_TYPE(let code, _, _), .INVALID_TOKEN_TYPE(let code, _, _), .MISSING_TABLE_NAME_IN_USERT_OPTION(let code, _, _),.MISSING_COLUMN_NAME_IN_USERT_OPTION(let code, _, _), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .APIError(let code, _), .INVALID_COLUMN_NAME(let code, _, _), .UNMOUNTED_COLLECT_ELEMENT(let code, _, _), .UNMOUNTED_REVEAL_ELEMENT(let code, _, _), .ERROR_TRIGGERED(let code, _, _), .DUPLICATE_ADDITIONAL_FIELD_FOUND(let code, _, _), .EMPTY_RECORD_COLUMN_VALUES(code: let code, message: _, value: _), .EMPTY_COLUMN_VALUE(code: let code, message: _, value: _),.DUPLICATE_ELEMENT_FOUND(let code, _, _) : + case .EMPTY_IDS(let code, _, _), .EMPTY_ID_VALUE(let code, _, _), .EMPTY_SKYFLOW_ID(let code, _, _), .INVALID_TOKEN_GROUP_REDACTION_ENTRY(let code, _, _), .TABLE_KEY_ERROR(let code, _, _), .FIELDS_KEY_ERROR(let code, _, _), .EMPTY_FIELDS_KEY(let code, _, _), .REDACTION_KEY_ERROR(let code, _, _), .MISSING_KEY_IDS(let code, _, _), .INVALID_TABLE_NAME_TYPE(let code, _, _), .INVALID_FIELDS_TYPE(let code, _, _), .INVALID_TOKEN_TYPE(let code, _, _), .MISSING_TABLE_NAME_IN_USERT_OPTION(let code, _, _), .MISSING_COLUMN_NAME_IN_USERT_OPTION(let code, _, _), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .APIError(let code, _), .INVALID_COLUMN_NAME(let code, _, _), .UNMOUNTED_COLLECT_ELEMENT(let code, _, _), .UNMOUNTED_REVEAL_ELEMENT(let code, _, _), .ERROR_TRIGGERED(let code, _, _), .DUPLICATE_ADDITIONAL_FIELD_FOUND(let code, _, _), .EMPTY_RECORD_COLUMN_VALUES(code: let code, message: _, value: _), .EMPTY_COLUMN_VALUE(code: let code, message: _, value: _),.DUPLICATE_ELEMENT_FOUND(let code, _, _) : return code } } - internal var description: String { + package var description: String { switch self { // No Formatting required // swiftlint:disable:next line_length - case .APIError(_, let message), .INVALID_REDACTION_TYPE( _, let message), .EMPTY_COLUMN_NAME_IN_COLLECT( _, let message), .EMPTY_TABLE_NAME_IN_COLLECT( _, let message), .REGEX_MATCH_FAILED( _, let message), .EMPTY_TABLE_NAME( _, let message), .EMPTY_VAULT_ID( _, let message), .EMPTY_VAULT_URL( _, let message), .RECORDS_KEY_ERROR( _, let message),.INVALID_RECORDS_TYPE( _, let message), .EMPTY_COLUMN_NAME( _, let message), .INVALID_BEARER_TOKEN_FORMAT( _, let message), .MISSING_RECORDS_ARRAY( _, let message), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS( _, let message), .EMPTY_RECORDS_OBJECT( _, let message), .MISSING_RECORDS_IN_GETBYID( _, let message),.INVALID_URL( _, let message),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED( _, let message), .MISSING_IDS_OR_COLUMN_VALUES_IN_GET( _, let message), .MISSING_RECORD_COLUMN_VALUE( _, let message), .UPSERT_OPTION_CANNOT_BE_EMPTY( _, let message),.INVALID_COLUMN_VALUES_IN_GET( _, let message),.MISSING_COMPOSABLE_LAYOUT_KEY(_, let message), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(_, let message),.EMPTY_TOKEN_ID( _, let message), .ID_KEY_ERROR( _, let message),.REDACTION_WITH_TOKEN_NOT_SUPPORTED( _, let message), .TOKENS_GET_COLUMN_NOT_SUPPPORTED( _, let message),.MISSING_COLUMN_NAME( _, let message),.EMPTY_COMPOSABLE_LAYOUT_ARRAY(_, let message), .INVALID_IDS_TYPE( _, let message),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: _, message: let message): + case .APIError(_, let message), .INVALID_REDACTION_TYPE( _, let message), .EMPTY_COLUMN_NAME_IN_COLLECT( _, let message), .EMPTY_TABLE_NAME_IN_COLLECT( _, let message), .REGEX_MATCH_FAILED( _, let message), .EMPTY_TABLE_NAME( _, let message), .EMPTY_VAULT_ID( _, let message), .EMPTY_VAULT_URL( _, let message), .RECORDS_KEY_ERROR( _, let message),.INVALID_RECORDS_TYPE( _, let message), .EMPTY_COLUMN_NAME( _, let message), .INVALID_BEARER_TOKEN_FORMAT( _, let message), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS( _, let message), .EMPTY_RECORDS_OBJECT( _, let message), .MISSING_RECORDS_IN_GETBYID( _, let message),.INVALID_URL( _, let message),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED( _, let message), .MISSING_IDS_OR_COLUMN_VALUES_IN_GET( _, let message), .MISSING_RECORD_COLUMN_VALUE( _, let message), .UPSERT_OPTION_CANNOT_BE_EMPTY( _, let message),.INVALID_COLUMN_VALUES_IN_GET( _, let message),.MISSING_COMPOSABLE_LAYOUT_KEY(_, let message), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(_, let message),.EMPTY_TOKEN_ID( _, let message), .ID_KEY_ERROR( _, let message),.REDACTION_WITH_TOKEN_NOT_SUPPORTED( _, let message), .TOKENS_GET_COLUMN_NOT_SUPPPORTED( _, let message),.MISSING_COLUMN_NAME( _, let message),.EMPTY_COMPOSABLE_LAYOUT_ARRAY(_, let message), .INVALID_IDS_TYPE( _, let message),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: _, message: let message): return message // Single value formatting // swiftlint:disable:next line_length - case .UNMOUNTED_COLLECT_ELEMENT( _, let message, let value), .UNMOUNTED_REVEAL_ELEMENT( _, let message, let value), .EMPTY_IDS( _, let message, let value), .EMPTY_ID_VALUE( _, let message, let value), .TABLE_KEY_ERROR( _, let message, let value), .FIELDS_KEY_ERROR( _, let message, let value), .EMPTY_FIELDS_KEY( _, let message, let value), .REDACTION_KEY_ERROR( _, let message, let value), .MISSING_KEY_IDS( _, let message, let value), .INVALID_TABLE_NAME_TYPE( _, let message, let value), .INVALID_FIELDS_TYPE( _, let message, let value), .INVALID_TOKEN_TYPE( _, let message, let value), .MISSING_TABLE_NAME_IN_USERT_OPTION( _, let message, let value), .MISSING_COLUMN_NAME_IN_USERT_OPTION( _, let message, let value), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION( _, let message, let value), .EMPTY_RECORD_COLUMN_VALUES( _, let message, let value), .EMPTY_COLUMN_VALUE( _, let message, let value), .INVALID_COLUMN_NAME( _, let message, let value), .DUPLICATE_ADDITIONAL_FIELD_FOUND( _, let message, let value), .ERROR_TRIGGERED( _, let message, let value): + case .UNMOUNTED_COLLECT_ELEMENT( _, let message, let value), .UNMOUNTED_REVEAL_ELEMENT( _, let message, let value), .EMPTY_IDS( _, let message, let value), .EMPTY_ID_VALUE( _, let message, let value), .EMPTY_SKYFLOW_ID( _, let message, let value), .INVALID_TOKEN_GROUP_REDACTION_ENTRY( _, let message, let value), .TABLE_KEY_ERROR( _, let message, let value), .FIELDS_KEY_ERROR( _, let message, let value), .EMPTY_FIELDS_KEY( _, let message, let value), .REDACTION_KEY_ERROR( _, let message, let value), .MISSING_KEY_IDS( _, let message, let value), .INVALID_TABLE_NAME_TYPE( _, let message, let value), .INVALID_FIELDS_TYPE( _, let message, let value), .INVALID_TOKEN_TYPE( _, let message, let value), .MISSING_TABLE_NAME_IN_USERT_OPTION( _, let message, let value), .MISSING_COLUMN_NAME_IN_USERT_OPTION( _, let message, let value), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION( _, let message, let value), .EMPTY_RECORD_COLUMN_VALUES( _, let message, let value), .EMPTY_COLUMN_VALUE( _, let message, let value), .INVALID_COLUMN_NAME( _, let message, let value), .DUPLICATE_ADDITIONAL_FIELD_FOUND( _, let message, let value), .ERROR_TRIGGERED( _, let message, let value): return formatMessage(message, [value]) // Multi value formatting case .DUPLICATE_ELEMENT_FOUND( _, let message, let values): return formatMessage(message, values) } } - internal var errorObject: NSError { + package var errorObject: NSError { NSError(domain: "", code: self.code, userInfo: [NSLocalizedDescriptionKey: self.description]) } - internal func getErrorObject(contextOptions: ContextOptions) -> NSError { + package func getErrorObject(contextOptions: ContextOptions) -> NSError { Log.error(message: self.description, contextOptions: contextOptions) return SkyflowError(domain: "", code: self.code, userInfo: [NSLocalizedDescriptionKey: "\(self.description)" ]) } @@ -121,7 +123,7 @@ internal enum ErrorCodes: CustomStringConvertible { } else { result += values[valuesIndex] } - + valuesIndex += 1 } else { result += word @@ -134,13 +136,102 @@ internal enum ErrorCodes: CustomStringConvertible { } public class SkyflowError: NSError { + // Retained for source compatibility with pre-split releases. var xml: String = "" - + func setXML(xml: String) { self.xml = xml } - + public func getXML() -> String { return self.xml } + + // Only populated for a whole-request API failure (see init(apiError:)) - nil for + // client-side validation failures, which only have httpCode/message. + public let grpcCode: Int? + public let httpStatus: String? + public let details: [Any]? + + // Aliases for NSError's own code/localizedDescription, matching the API error JSON's key names. + public var httpCode: Int { self.code } + public var message: String { self.localizedDescription } + + public init(domain: String, code: Int, userInfo: [String: Any]? = nil, + grpcCode: Int? = nil, httpStatus: String? = nil, details: [Any]? = nil) { + self.grpcCode = grpcCode + self.httpStatus = httpStatus + self.details = details + super.init(domain: domain, code: code, userInfo: userInfo) + } + + public required init?(coder: NSCoder) { + self.grpcCode = coder.decodeObject(forKey: "grpcCode") as? Int + self.httpStatus = coder.decodeObject(forKey: "httpStatus") as? String + self.details = coder.decodeObject(forKey: "details") as? [Any] + super.init(coder: coder) + } + + // Builds a SkyflowError out of a whole-request API failure of the shape + // {"error": {"grpcCode", "httpCode", "message", "httpStatus", "details"}} - returns nil if + // apiError isn't in that shape (e.g. it's already a SkyflowError from client-side validation). + // httpCode maps to NSError's own `code`, and message maps to localizedDescription/NSLocalizedDescriptionKey. + public convenience init?(apiError: Any) { + guard let dict = apiError as? [String: Any], + let detail = dict["error"] as? [String: Any] else { return nil } + self.init( + domain: "", + code: detail["httpCode"] as? Int ?? 0, + userInfo: [NSLocalizedDescriptionKey: detail["message"] as? String ?? ""], + grpcCode: detail["grpcCode"] as? Int, + httpStatus: detail["httpStatus"] as? String, + details: detail["details"] as? [Any] + ) + } + + // Normalizes any Callback.onFailure payload into a SkyflowError, so CollectCallback/RevealCallback + // can type onFailure as (SkyflowError) -> Void instead of (Any) -> Void. + package static func wrap(_ error: Any) -> SkyflowError { + if let skyflowError = error as? SkyflowError { + return skyflowError + } + if let apiError = SkyflowError(apiError: error) { + return apiError + } + if let nsError = error as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + // Client.detokenize() bypasses RevealValueCallback entirely, so whole-request failures + // arrive here still wrapped in an "errors" array instead of a raw NSError. Two shapes are + // possible depending on where the failure originated: + // - Client.swift's own pre-network validation (empty vaultID/vaultURL/records key): each + // entry in "errors" IS the NSError directly. + // - FlowVaultRevealAPICallback's post-dispatch failures (invalid bearer token, network + // error, invalid URL, etc.): each entry is {"error": }. + // Recover the underlying NSError instead of stringifying the whole container dict. + // (Container.reveal()'s equivalent failures are already unwrapped earlier, + // by RevealValueCallback.onFailure.) + // + // A third shape reaches here too: FlowVaultInsertAPICallback's insert+update merge + // (group.notify in onSuccess) builds mergedErrors entries as {"error": {"message", + // "httpCode", ...}} - a JSON-decoded API error detail dict, not an NSError - so it's + // handled by trying SkyflowError(apiError:) on the entry itself, which expects exactly + // that {"error": {...}} shape. + if let dict = error as? [String: Any], let errorsArray = dict["errors"] as? [Any] { + for entry in errorsArray { + if let nsError = entry as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + if let nested = entry as? [String: Any] { + if let nsError = nested["error"] as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + if let apiError = SkyflowError(apiError: entry) { + return apiError + } + } + } + } + return SkyflowError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(error)"]) + } } diff --git a/Sources/SkyflowCore/reveal/BaseRevealElementInput.swift b/Sources/SkyflowCore/reveal/BaseRevealElementInput.swift new file mode 100644 index 00000000..bea96c7c --- /dev/null +++ b/Sources/SkyflowCore/reveal/BaseRevealElementInput.swift @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared storage for the reveal element input. Each SDK product defines its +// own public `RevealElementInput` struct exposing that contract's initializers +// (the legacy SDK includes a per-token `redaction:` parameter; FlowVault does +// not); those structs wrap this value and hand it to the core element +// factories. Value semantics: every element stores its own copy, so +// post-creation mutations (setToken/setAltText) never leak between elements. + +import Foundation + +package struct BaseRevealElementInput { + package var token: String + package var inputStyles: Styles? + package var labelStyles: Styles? + package var errorTextStyles: Styles? + package var label: String + // Per-token redaction is only surfaced (and sent on the wire) by the + // legacy SDK; FlowVault leaves the default and uses tokenGroupRedactions. + package var redaction: RedactionType + package var altText: String + + package init(token: String, inputStyles: Styles, labelStyles: Styles, errorTextStyles: Styles, label: String, redaction: RedactionType, altText: String) { + self.token = token + self.inputStyles = inputStyles + self.labelStyles = labelStyles + self.errorTextStyles = errorTextStyles + self.label = label + self.redaction = redaction + self.altText = altText + } +} diff --git a/Sources/SkyflowCore/reveal/BaseRevealElementOptions.swift b/Sources/SkyflowCore/reveal/BaseRevealElementOptions.swift new file mode 100644 index 00000000..b2c9b86a --- /dev/null +++ b/Sources/SkyflowCore/reveal/BaseRevealElementOptions.swift @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared storage for the reveal element options. Each SDK product defines its +// own public `BaseRevealElementOptions` struct exposing that contract's +// initializer (identical in both contracts today). + +import Foundation + +package struct BaseRevealElementOptions { + package var format: String? + package var translation: [ Character: String ]? + package var enableCopy: Bool? + + package init(format: String? = nil, translation: [ Character: String ]? = nil, enableCopy: Bool? = false) { + self.format = format + self.translation = translation + self.enableCopy = enableCopy + if (self.translation != nil){ + for (_, value) in self.translation! { + if value == "" { + var contextOptions = ContextOptions() + contextOptions.interface = .REVEAL_CONTAINER + contextOptions.logLevel = .WARN + Log.warn(message: .EMPTY_TRANSLATION_VALUE, values: [], contextOptions: contextOptions) + } + } + + } + } + +} diff --git a/Sources/Skyflow/reveal/ConversionHelpers.swift b/Sources/SkyflowCore/reveal/ConversionHelpers.swift similarity index 79% rename from Sources/Skyflow/reveal/ConversionHelpers.swift rename to Sources/SkyflowCore/reveal/ConversionHelpers.swift index a70d8134..560fc607 100644 --- a/Sources/Skyflow/reveal/ConversionHelpers.swift +++ b/Sources/SkyflowCore/reveal/ConversionHelpers.swift @@ -7,8 +7,8 @@ import Foundation -class ConversionHelpers { - static func checkElementsAreMounted(elements: [Any]) -> Any? { +package class ConversionHelpers { + package static func checkElementsAreMounted(elements: [Any]) -> Any? { for element in elements { if let label = element as? Label, !label.isMounted() { return label diff --git a/Sources/Skyflow/reveal/FormatLabel.swift b/Sources/SkyflowCore/reveal/FormatLabel.swift similarity index 100% rename from Sources/Skyflow/reveal/FormatLabel.swift rename to Sources/SkyflowCore/reveal/FormatLabel.swift diff --git a/Sources/Skyflow/reveal/Label.swift b/Sources/SkyflowCore/reveal/Label.swift similarity index 93% rename from Sources/Skyflow/reveal/Label.swift rename to Sources/SkyflowCore/reveal/Label.swift index 5b4fe241..6d818623 100644 --- a/Sources/Skyflow/reveal/Label.swift +++ b/Sources/SkyflowCore/reveal/Label.swift @@ -12,14 +12,14 @@ import UIKit public class Label: UIView, Element, BaseElement { internal var skyflowLabelView: SkyflowLabelView! - internal var revealInput: RevealElementInput! - internal var options: RevealElementOptions! + package var revealInput: BaseRevealElementInput! + internal var options: BaseRevealElementOptions! internal var stackView = UIStackView() internal var labelField = PaddingLabel(frame: .zero) internal var errorMessage = PaddingLabel(frame: .zero) - internal var errorTriggered: Bool = false - internal var triggeredErrorMessage: String = "" + package var errorTriggered: Bool = false + package var triggeredErrorMessage: String = "" internal var uuid: String = "" internal var actualValue: String? = nil @@ -28,7 +28,7 @@ public class Label: UIView, Element, BaseElement { internal var verticalConstraint = [NSLayoutConstraint]() - internal init(input: RevealElementInput, options: RevealElementOptions) { + internal init(input: BaseRevealElementInput, options: BaseRevealElementOptions) { self.skyflowLabelView = SkyflowLabelView(input: input, options: options) super.init(frame: CGRect()) self.revealInput = input @@ -96,7 +96,7 @@ public class Label: UIView, Element, BaseElement { return output } - internal func updateVal(value: String) { + package func updateVal(value: String) { if let format = self.options?.format { if format != "" { if(self.options.translation == nil){ @@ -122,7 +122,7 @@ public class Label: UIView, Element, BaseElement { } } - internal func isMounted() -> Bool { + package func isMounted() -> Bool { var flag = false if Thread.isMainThread { flag = self.window != nil @@ -184,19 +184,19 @@ public class Label: UIView, Element, BaseElement { NSLayoutConstraint.activate(verticalConstraint) } - func showError(message: String) { + package func showError(message: String) { self.errorMessage.text = message self.skyflowLabelView.updateStyle() self.errorMessage.alpha = 1.0 } - func hideError() { + package func hideError() { if !self.errorTriggered { self.errorMessage.alpha = 0.0 } } - internal func getValue() -> String { + package func getValue() -> String { return self.actualValue ?? "" } @@ -232,11 +232,11 @@ public class Label: UIView, Element, BaseElement { self.skyflowLabelView.updateVal(value: actualValue == nil ? revealInput.token : actualValue!, actualValue: nil) } - internal func getToken() -> String{ + package func getToken() -> String{ return self.revealInput.token } - internal func getValueForConnections() -> String { + package func getValueForConnections() -> String { if actualValue != nil { return getValue() } else { diff --git a/Sources/Skyflow/reveal/RedactionType.swift b/Sources/SkyflowCore/reveal/RedactionType.swift similarity index 100% rename from Sources/Skyflow/reveal/RedactionType.swift rename to Sources/SkyflowCore/reveal/RedactionType.swift diff --git a/Sources/SkyflowCore/reveal/RevealContainer.swift b/Sources/SkyflowCore/reveal/RevealContainer.swift new file mode 100644 index 00000000..2a201472 --- /dev/null +++ b/Sources/SkyflowCore/reveal/RevealContainer.swift @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Reveal container marker type and the shared element factory. +// The public create(input:options:) and the contract-specific reveal() +// operation are defined per SDK target; create() forwards here. + +import Foundation + +public class RevealContainer: ContainerProtocol { +} + +extension Container { + package func makeRevealElement(input: BaseRevealElementInput, options: BaseRevealElementOptions? = BaseRevealElementOptions()) -> Label where T: RevealContainer { + var tempContextOptions = self.skyflow.contextOptions + tempContextOptions.interface = .REVEAL_CONTAINER + let revealElement = Label(input: input, options: options!) + revealElements.append(revealElement) + let uuid = NSUUID().uuidString + self.skyflow.elementLookup[uuid] = revealElement + revealElement.uuid = uuid + Log.info(message: .CREATED_ELEMENT, values: [input.label == "" ? "reveal" : input.label], contextOptions: tempContextOptions) + return revealElement + } +} diff --git a/Sources/Skyflow/reveal/RevealErrorRecord.swift b/Sources/SkyflowCore/reveal/RevealErrorRecord.swift similarity index 54% rename from Sources/Skyflow/reveal/RevealErrorRecord.swift rename to Sources/SkyflowCore/reveal/RevealErrorRecord.swift index 8e51c9b4..dea1623c 100644 --- a/Sources/Skyflow/reveal/RevealErrorRecord.swift +++ b/Sources/SkyflowCore/reveal/RevealErrorRecord.swift @@ -6,11 +6,11 @@ import Foundation -class RevealErrorRecord { - var id: String - var error: NSError +package class RevealErrorRecord { + package var id: String + package var error: NSError - init(id: String, error: NSError) { + package init(id: String, error: NSError) { self.id = id self.error = error } diff --git a/Sources/Skyflow/reveal/RevealSuccessRecord.swift b/Sources/SkyflowCore/reveal/RevealSuccessRecord.swift similarity index 54% rename from Sources/Skyflow/reveal/RevealSuccessRecord.swift rename to Sources/SkyflowCore/reveal/RevealSuccessRecord.swift index 3a3cf8ad..f9ebb873 100644 --- a/Sources/Skyflow/reveal/RevealSuccessRecord.swift +++ b/Sources/SkyflowCore/reveal/RevealSuccessRecord.swift @@ -6,10 +6,10 @@ import Foundation -struct RevealSuccessRecord { - var token_id: String - var value: String - init(token_id: String, value: String) { +package struct RevealSuccessRecord { + package var token_id: String + package var value: String + package init(token_id: String, value: String) { self.token_id = token_id self.value = value } diff --git a/Sources/Skyflow/reveal/SkyflowLabelView.swift b/Sources/SkyflowCore/reveal/SkyflowLabelView.swift similarity index 96% rename from Sources/Skyflow/reveal/SkyflowLabelView.swift rename to Sources/SkyflowCore/reveal/SkyflowLabelView.swift index 9cebc71d..60708b54 100644 --- a/Sources/Skyflow/reveal/SkyflowLabelView.swift +++ b/Sources/SkyflowCore/reveal/SkyflowLabelView.swift @@ -9,8 +9,8 @@ import UIKit public class SkyflowLabelView: UIView { internal var label = FormatLabel(frame: .zero) - internal var revealInput: RevealElementInput! - internal var options: RevealElementOptions! + internal var revealInput: BaseRevealElementInput! + internal var options: BaseRevealElementOptions! internal var horizontalConstraints = [NSLayoutConstraint]() internal var verticalConstraint = [NSLayoutConstraint]() @@ -99,7 +99,7 @@ public class SkyflowLabelView: UIView { } } - internal init(input: RevealElementInput, options: RevealElementOptions) { + internal init(input: BaseRevealElementInput, options: BaseRevealElementOptions) { super.init(frame: CGRect()) self.revealInput = input self.options = options diff --git a/Sources/Skyflow/reveal/UILabel+Copyable.swift b/Sources/SkyflowCore/reveal/UILabel+Copyable.swift similarity index 97% rename from Sources/Skyflow/reveal/UILabel+Copyable.swift rename to Sources/SkyflowCore/reveal/UILabel+Copyable.swift index 95e67dcf..72c64b0e 100644 --- a/Sources/Skyflow/reveal/UILabel+Copyable.swift +++ b/Sources/SkyflowCore/reveal/UILabel+Copyable.swift @@ -104,7 +104,7 @@ public extension UILabel { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var image = UIImage(named: "Success-Icon", in: resourceBundle, compatibleWith: nil) #endif @@ -117,7 +117,7 @@ public extension UILabel { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var copyImage = UIImage(named: "Copy-Icon", in: resourceBundle, compatibleWith: nil) #endif @@ -135,7 +135,7 @@ public extension UILabel { #else let frameworkBundle = Bundle(for: TextField.self) var bundleURL = frameworkBundle.resourceURL - bundleURL!.appendPathComponent("Skyflow.bundle") + bundleURL!.appendPathComponent(SkyflowAssetBundle.name) let resourceBundle = Bundle(url: bundleURL!) var image = UIImage(named: "Copy-Icon", in: resourceBundle, compatibleWith: nil) #endif diff --git a/Sources/Skyflow/utils/DictionaryExtension.swift b/Sources/SkyflowCore/utils/DictionaryExtension.swift similarity index 96% rename from Sources/Skyflow/utils/DictionaryExtension.swift rename to Sources/SkyflowCore/utils/DictionaryExtension.swift index 2612e79d..03db0d80 100644 --- a/Sources/Skyflow/utils/DictionaryExtension.swift +++ b/Sources/SkyflowCore/utils/DictionaryExtension.swift @@ -12,7 +12,7 @@ import Foundation extension Dictionary { - subscript(keyPath keyPath: String) -> Any? { + package subscript(keyPath keyPath: String) -> Any? { get { guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath) else { return nil } diff --git a/Sources/Skyflow/utils/FetchMetrices.swift b/Sources/SkyflowCore/utils/FetchMetrices.swift similarity index 81% rename from Sources/Skyflow/utils/FetchMetrices.swift rename to Sources/SkyflowCore/utils/FetchMetrices.swift index 82cd9990..4d20ac17 100644 --- a/Sources/Skyflow/utils/FetchMetrices.swift +++ b/Sources/SkyflowCore/utils/FetchMetrices.swift @@ -8,9 +8,10 @@ import Foundation import UIKit -internal class FetchMetrices { +package class FetchMetrices { + package init() {} - internal func getDeviceDetails() -> [String: Any] { + package func getDeviceDetails() -> [String: Any] { var deviceDetails: [String: Any] = [:] do { let currentDevice = UIDevice.current @@ -21,7 +22,7 @@ internal class FetchMetrices { let systemVersion = currentDevice.systemVersion deviceDetails["os_details"] = systemName + "@" + systemVersion - deviceDetails["sdk_name_version"] = "skyflow-iOS@" + SDK_VERSION + deviceDetails["sdk_name_version"] = SDK_NAME + "@" + SDK_VERSION } catch { deviceDetails["device"] = "" deviceDetails["os_details"] = "" @@ -31,7 +32,7 @@ internal class FetchMetrices { } - internal func getMetrices() -> [String: Any]{ + package func getMetrices() -> [String: Any]{ let details = getDeviceDetails() let deviceDetails = [ "sdk_name_version": details["sdk_name_version"] , diff --git a/Sources/SkyflowCore/utils/SkyflowAssetBundle.swift b/Sources/SkyflowCore/utils/SkyflowAssetBundle.swift new file mode 100644 index 00000000..4902ca3e --- /dev/null +++ b/Sources/SkyflowCore/utils/SkyflowAssetBundle.swift @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// CocoaPods resource-bundle name for asset lookups in the shared core. +// Both pods ship their assets as "Skyflow.bundle" (declared in each podspec's +// resource_bundles) so the lookup is identical regardless of which product is +// built. (SPM builds use Bundle.module instead and never hit this path.) + +import Foundation + +package enum SkyflowAssetBundle { + package static let name = "Skyflow.bundle" +} diff --git a/Sources/Skyflow/utils/StringExtension.swift b/Sources/SkyflowCore/utils/StringExtension.swift similarity index 98% rename from Sources/Skyflow/utils/StringExtension.swift rename to Sources/SkyflowCore/utils/StringExtension.swift index b2d8dcd0..efe3ba19 100644 --- a/Sources/Skyflow/utils/StringExtension.swift +++ b/Sources/SkyflowCore/utils/StringExtension.swift @@ -12,9 +12,6 @@ import Foundation extension String { - - - func getFirstRegexMatch(of regex: String, contextOptions: ContextOptions) throws -> String { guard let range = self.range(of: regex, options: .regularExpression) else { throw ErrorCodes.REGEX_MATCH_FAILED().errorObject } diff --git a/Sources/SkyflowFlowVaultIOS/Exports.swift b/Sources/SkyflowFlowVaultIOS/Exports.swift new file mode 100644 index 00000000..b24c9864 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/Exports.swift @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Re-export the shared core so SDK consumers only need `import SkyflowFlowVaultIOS`. + +@_exported import SkyflowCore diff --git a/Sources/SkyflowFlowVaultIOS/Init.swift b/Sources/SkyflowFlowVaultIOS/Init.swift new file mode 100644 index 00000000..b42b4afe --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/Init.swift @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Initialize the FlowVault SDK, SkyflowFlowVaultIOS.initialize + +import Foundation + +public func initialize(_ skyflowConfig: Configuration) -> Client { + SDK_NAME = "skyflow-flowvault-ios" + return Client(skyflowConfig) +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/AdditionalFields.swift b/Sources/SkyflowFlowVaultIOS/collect/AdditionalFields.swift new file mode 100644 index 00000000..659ec242 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/AdditionalFields.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Typed replacement for the old [String: Any] additionalFields shape on CollectOptions - +// non-PCI records to insert/update alongside whatever's collected from mounted elements. + +import Foundation + +public struct AdditionalFieldsRecord { + public let tableName: String + public let data: [String: Any] + // Present to update an existing record, absent to insert a new one. + public let skyflowId: String? + + public init(tableName: String, data: [String: Any], skyflowId: String? = nil) { + self.tableName = tableName + self.data = data + self.skyflowId = skyflowId + } +} + +public struct AdditionalFields { + public let records: [AdditionalFieldsRecord] + + public init(records: [AdditionalFieldsRecord]) { + self.records = records + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift b/Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift new file mode 100644 index 00000000..73465dd8 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Generates a mock CVV placeholder used to mask the real CVV token in collect responses (FlowDB v2). + +import Foundation + +internal enum CVVMockGenerator { + internal static func generateMockCVV(length: Int, actualValue: String) -> String { + guard length > 0 else { return "" } + var mock: String + repeat { + mock = randomNumericString(length: length) + } while mock == actualValue + return mock + } + + private static func randomNumericString(length: Int) -> String { + var bytes = [UInt8](repeating: 0, count: length) + let status = SecRandomCopyBytes(kSecRandomDefault, length, &bytes) + if status != errSecSuccess { + var generator = SystemRandomNumberGenerator() + for i in 0.. entered value, keyed by table name (inserts) or skyflowId (updates). +internal struct CVVCaptureMap { + var byTable: [String: [String: String]] = [:] + var byRecordId: [String: [String: String]] = [:] + + var isEmpty: Bool { byTable.isEmpty && byRecordId.isEmpty } +} + +internal enum CVVTokenReplacer { + // An optional CVV element left blank still submits "" to the vault and can get a real token + // back for it - always capture the column (even when empty) so that token gets replaced with + // "" too (see applyMock below), rather than leaking the real vault token unmasked. + // + // First element wins per (table/recordId, column) - mirrors the existing dedup behavior in + // FlowVaultCollectRequestBody, where a second element sharing a column (ElementValueMatchRule) + // is dropped rather than merged. + internal static func captureCVVMap(elements: [TextField]) -> CVVCaptureMap { + var map = CVVCaptureMap() + for element in elements { + guard element.fieldType == .CVV, let columnName = element.columnName else { continue } + let value = element.getValue() + + if let skyflowId = element.skyflowId, !skyflowId.isEmpty { + if map.byRecordId[skyflowId]?[columnName] == nil { + map.byRecordId[skyflowId, default: [:]][columnName] = value + } + } else if let tableName = element.tableName { + if map.byTable[tableName]?[columnName] == nil { + map.byTable[tableName, default: [:]][columnName] = value + } + } + } + return map + } + + internal static func replaceCVVTokens(in records: [[String: Any]], cvvMap: CVVCaptureMap) -> [[String: Any]] { + guard !cvvMap.isEmpty else { return records } + + return records.map { record in + guard record["error"] == nil, var fields = record["fields"] as? [String: Any] else { return record } + + let cvvColumns: [String: String]? + if let skyflowId = record["skyflowID"] as? String, let byId = cvvMap.byRecordId[skyflowId] { + cvvColumns = byId + } else if let tableName = record["tableName"] as? String, let byTable = cvvMap.byTable[tableName] { + cvvColumns = byTable + } else { + cvvColumns = nil + } + + guard let columns = cvvColumns else { return record } + + for (capturedColumn, enteredValue) in columns { + applyMock(to: &fields, capturedColumn: capturedColumn, enteredValue: enteredValue) + } + + var record = record + record["fields"] = fields + return record + } + } + + // Splits a (possibly nested) captured column at the first "." into a top-level response key + // and a nested path, then replaces the token(s) matching that key/path in the response's raw + // token-entry list for that column, leaving every other entry (and every other column) intact. + private static func applyMock(to fields: inout [String: Any], capturedColumn: String, enteredValue: String) { + let parts = capturedColumn.split(separator: ".", maxSplits: 1).map(String.init) + let topKey = parts[0] + let nestedPath = parts.count > 1 ? parts[1] : nil + + guard var entries = fields[topKey] as? [[String: Any]] else { return } + + // A blank entered CVV has no length to match, and generateMockCVV(length: 0, ...) would + // never terminate its "regenerate until different" loop (every draw is "", which always + // equals the empty entered value) - never call it here; just replace with "" directly. + let mock = enteredValue.isEmpty ? "" : CVVMockGenerator.generateMockCVV(length: enteredValue.count, actualValue: enteredValue) + + for index in entries.indices { + let entryPath = entries[index]["path"] as? String + if nestedPath == nil { + if entryPath == nil { + entries[index]["token"] = mock + } + } else if entryPath == nestedPath { + entries[index]["token"] = mock + } + } + + fields[topKey] = entries + } +} + +// Wraps another Callback and swaps CVV tokens in "records" (if present) before forwarding the +// response onward. Inserted between the container's collect() and its existing LogCallback, so +// no changes are needed to the network/response-parsing classes or APIClient. +internal class CVVMaskingCallback: Callback { + private let cvvMap: CVVCaptureMap + private let wrapped: Callback + + internal init(cvvMap: CVVCaptureMap, wrapping callback: Callback) { + self.cvvMap = cvvMap + self.wrapped = callback + } + + internal func onSuccess(_ responseBody: Any) { + wrapped.onSuccess(mask(responseBody)) + } + + internal func onFailure(_ error: Any) { + wrapped.onFailure(mask(error)) + } + + // A partial-batch failure can still carry a "records" array alongside "errors", so both + // onSuccess and onFailure bodies need the same masking. + private func mask(_ body: Any) -> Any { + guard cvvMap.isEmpty == false, + var dict = body as? [String: Any], + let records = dict["records"] as? [[String: Any]] else { return body } + + dict["records"] = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + return dict + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift b/Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift new file mode 100644 index 00000000..95dd9c7d --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift @@ -0,0 +1,112 @@ +/* Copyright (c) 2022 Skyflow +*/ + +// FlowVault implementation of the collect() operation. + +import Foundation +import UIKit + +public extension Container { + func collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { + var tempContextOptions = self.skyflow.contextOptions + tempContextOptions.interface = .COLLECT_CONTAINER + if self.skyflow.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.skyflow.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + var errors = "" + var errorCode: ErrorCodes? + Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) + + for element in self.elements { + errorCode = checkElement(element: element) + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + + let state = element.getState() + let error = state["validationError"] + if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { + errors += element.columnName + " is empty" + "\n" + element.updateErrorMessage() + } + if !(state["isValid"] as! Bool) { + errors += "for " + element.columnName + " " + (error as! String) + "\n" + } + if element.isFirstResponder { + element.resignFirstResponder() + } + } + if errors != "" { + callback.onFailure(SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + return + } + if let additionalFields = options?.additionalFields { + if additionalFields.records.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + for (index, record) in additionalFields.records.enumerated() { + errorCode = checkRecord(record: record, index: index) + if errorCode != nil { + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + } + let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + if options?.upsert != nil { + if icOptions.validateUpsert() { + return; + } + } + if records != nil { + let logCallback = LogCallback(clientCallback: callback, contextOptions: self.skyflow.contextOptions, + onSuccessHandler: { + Log.info(message: .COLLECT_SUBMIT_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + let cvvMap = CVVTokenReplacer.captureCVVMap(elements: self.elements) + let cvvMaskingCallback = CVVMaskingCallback(cvvMap: cvvMap, wrapping: logCallback) + self.skyflow.apiClient.postAndUpdate(records: records!, callback: cvvMaskingCallback, options: icOptions, contextOptions: tempContextOptions) + } + } + + private func checkElement(element: TextField) -> ErrorCodes? { + if element.collectInput.tableName.isEmpty { + return .EMPTY_TABLE_NAME_IN_COLLECT() + } + if element.collectInput.column.isEmpty { + return .EMPTY_COLUMN_NAME_IN_COLLECT() + } + if !element.isMounted() { + return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + } + + return nil + } + + private func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.tableName.isEmpty { + return .EMPTY_TABLE_NAME() + } + if record.data.isEmpty { + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + return nil + } +} + +public extension Container { + func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: CollectContainer { + return makeCollectElement(input: input.data, options: options?.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/CollectOptions.swift b/Sources/SkyflowFlowVaultIOS/collect/CollectOptions.swift new file mode 100644 index 00000000..922b1f44 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/CollectOptions.swift @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes the option of Collect + +import Foundation + +public struct CollectOptions { + var additionalFields: AdditionalFields? + var upsert: [UpsertOption]? + public init(additionalFields: AdditionalFields? = nil, upsert: [UpsertOption]? = nil) { + self.additionalFields = additionalFields + self.upsert = upsert + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/CollectResponse.swift b/Sources/SkyflowFlowVaultIOS/collect/CollectResponse.swift new file mode 100644 index 00000000..4aaea9f2 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/CollectResponse.swift @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +import Foundation + +public struct CollectResponse { + public let records: [CollectRecord] + + public init?(_ responseBody: Any) { + guard let dict = responseBody as? [String: Any], + let recordDicts = dict["records"] as? [[String: Any]] else { return nil } + self.records = recordDicts.map { CollectRecord($0) } + } +} + +// Each entry in "records" is either a successfully inserted/updated record (error is nil) or a +// failed one (error is non-nil) - the vault returns both together in the same array, each tagged +// with its own httpCode. +public struct CollectRecord { + public let tableName: String? + public let skyflowId: String? + // Keyed by column name, e.g. "card_number": [{"token": "...", "tokenGroupName": "..."}]. + public let tokens: [String: Any]? + // Keyed by column name, e.g. "card_number": [{"data": "...", "hashName": "..."}]. + public let hashedData: [String: Any]? + public let httpCode: Int + public let error: String? + + init(_ dict: [String: Any]) { + self.tableName = dict["tableName"] as? String + // Wire key is intentionally left as "skyflowID" here - only the public Swift-facing + // property name changed, not the response dict this reads from. + self.skyflowId = dict["skyflowID"] as? String + // Dict key is intentionally left as "fields" here - only the public Swift-facing + // property name changed, not the response dict this reads from. + self.tokens = dict["fields"] as? [String: Any] + self.hashedData = dict["hashedData"] as? [String: Any] + self.httpCode = dict["httpCode"] as? Int ?? 0 + self.error = dict["error"] as? String + } +} + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into CollectResponse +// for you. Use it directly with the original callback-based collect(callback:options:). +public class CollectCallback: Callback { + private let successHandler: (CollectResponse) -> Void + private let failureHandler: (SkyflowError) -> Void + + public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = CollectResponse(responseBody) else { + failureHandler(SkyflowError.wrap(responseBody)) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-record failure inside CollectResponse.records, and for client-side validation + // failures. Always normalized into a Skyflow.SkyflowError - see SkyflowError.wrap. + public func onFailure(_ error: Any) { + failureHandler(SkyflowError.wrap(error)) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift b/Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift new file mode 100644 index 00000000..e231f431 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// FlowVault implementation of the composable container's collect() operation. + +import Foundation +import UIKit + +public extension Container { + + func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { + var tempContextOptions = self.skyflow.contextOptions + tempContextOptions.interface = .COMPOSABLE_CONTAINER + if self.skyflow.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.skyflow.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + var errors = "" + var errorCode: ErrorCodes? + Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) + + for element in self.elements { + errorCode = checkComposableElement(element: element) + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + + let state = element.getState() + let error = state["validationError"] + if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { + errors += element.columnName + " is empty" + "\n" + element.updateErrorMessage() + } + if !(state["isValid"] as! Bool) { + errors += "for " + element.columnName + " " + (error as! String) + "\n" + } + if element.isFirstResponder { + element.resignFirstResponder() + } + } + + if errors != "" { + callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + + return + } + if let additionalFields = options?.additionalFields { + if additionalFields.records.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + for (index, record) in additionalFields.records.enumerated() { + errorCode = checkComposableRecord(record: record, index: index) + if errorCode != nil { + return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + } + let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + if options?.upsert != nil { + if icOptions.validateUpsert() { + return; + } + } + if records != nil { + let logCallback = LogCallback(clientCallback: callback, contextOptions: self.skyflow.contextOptions, + onSuccessHandler: { + Log.info(message: .COLLECT_SUBMIT_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + let cvvMap = CVVTokenReplacer.captureCVVMap(elements: self.elements) + let cvvMaskingCallback = CVVMaskingCallback(cvvMap: cvvMap, wrapping: logCallback) + self.skyflow.apiClient.postAndUpdate(records: records!, callback: cvvMaskingCallback, options: icOptions, contextOptions: tempContextOptions) + } + } + + private func checkComposableElement(element: TextField) -> ErrorCodes? { + if element.collectInput.tableName.isEmpty { + return .EMPTY_TABLE_NAME_IN_COLLECT() + } + if element.collectInput.column.isEmpty { + return .EMPTY_COLUMN_NAME_IN_COLLECT() + } + if !element.isMounted() { + return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + } + + return nil + } + + private func checkComposableRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.tableName.isEmpty { + return .EMPTY_TABLE_NAME() + } + if record.data.isEmpty { + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + return nil + } +} + +public extension Container { + func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: ComposableContainer { + return makeComposableElement(input: input.data, options: options?.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift new file mode 100644 index 00000000..ebfdb0a9 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Callback used while API callback for Collect the elements (FlowDB v2) + +import Foundation +import UIKit + +internal class FlowVaultCollectAPICallback: Callback { + var apiClient: APIClient + var records: [String: Any] + var callback: Callback + var options: FlowVaultICOptions + var contextOptions: ContextOptions + + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + self.records = records + self.apiClient = apiClient + self.callback = callback + self.options = options + self.contextOptions = contextOptions + } + + internal func onSuccess(_ responseBody: Any) { + let insertRecords = records["records"] as? [[String: Any]] ?? [] + let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) + let hasInsert = !insertRecords.isEmpty + let hasUpdate = !updateRecords.isEmpty + + if !hasInsert && !hasUpdate { + self.callback.onSuccess(["records": []]) + return + } + + // No update records: preserve the original single-call insert behavior exactly + // (including passing a full-request-level failure straight through, unwrapped). + if !hasUpdate { + guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + self.callback.onFailure(response) + return + } + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) + } catch { + self.callback.onFailure(error) + } + } + task.resume() + } catch let error { + self.callback.onFailure(error) + } + return + } + + guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + + let group = DispatchGroup() + var mergedRecords: [[String: Any]] = [] + var mergedErrors: [[String: Any]] = [] + + if hasInsert { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + if hasUpdate { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! + do { + let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + group.notify(queue: .main) { + if mergedErrors.isEmpty { + self.callback.onSuccess(["records": mergedRecords]) + } else { + self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) + } + } + } + + internal func onFailure(_ error: Any) { + self.callback.onFailure(error) + } + + internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { + var updateRecords: [[String: Any]] = [] + for (skyflowID, value) in updateDict { + guard let entry = value as? [String: Any], + let tableName = entry["table"] as? String, + let fields = entry["fields"] as? [String: Any] else { continue } + updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) + } + return updateRecords + } + + internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { + var temp: [String: Any] = [:] + for (key, val) in dict { + if let v = val as? [String: Any] { + temp[key] = buildFieldsDict(dict: v) + } else { + temp[key] = val + } + } + return temp + } + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + + } + + internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every record in the batch fails, + // but the body still has the same {"records": [...]} shape as a success/partial + // response (each record carrying its own error/httpCode) - parse it as such. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["records"] != nil { + return try getCollectResponseBody(data: safeData) + } + var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] + } + // Pass the vault's structured error object (grpcCode/httpStatus/details) through + // as-is, only patching in the request-id and defaulting httpCode when absent. + if var errorDict = errorResponse["error"] as? [String: Any] { + if let message = errorDict["message"] as? String { + description = message + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorDict["message"] = description + } + if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } + return ["error": errorDict] + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } + return ["error": ["message": description, "httpCode": httpResponse.statusCode]] + } + } + + guard let safeData = data else { + return ["records": []] + } + + return try getCollectResponseBody(data: safeData) + + } + + func getCollectResponseBody(data: Data) throws -> [String: Any]{ + let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] + var records: [[String: Any]] = [] + + let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + records.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } + var fields: [String: Any] = [:] + if let tokens = entry["tokens"] as? [String: Any] { + for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { + fields[column] = tokenValue + } + } + successEntry["fields"] = fields + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + records.append(successEntry) + } + } + + return ["records": records] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift new file mode 100644 index 00000000..306b3cda --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Class for formatting the request body for collecting the data (FlowDB v2) + +import Foundation + +internal class FlowVaultCollectRequestBody { + static var tableSet: Set = Set() + static var callback: Callback? + static var breakFlag = false + static var mergedDict: [String: Any] = [:] + + internal static func addFieldsToTableSet(tableName: String, prefix: String, fields: [String: Any], contextOptions: ContextOptions) { + if !self.breakFlag { + for (key, val) in fields { + if val is [String: Any] { + addFieldsToTableSet(tableName: tableName, prefix: prefix == "" ? key : prefix + "." + key, fields: val as! [String: Any], contextOptions: contextOptions) + } else { + let tableSetEntry = tableName + "-" + (prefix == "" ? key : prefix + "." + key) + if tableSet.contains(tableSetEntry) { + if !self.breakFlag { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: key).getErrorObject(contextOptions: contextOptions)) + self.breakFlag = true + return + } + } else { + self.tableSet.insert(tableSetEntry) + } + } + } + } + } + + internal static func mergeFields(tableName: String, prefix: String, dict: [String: Any], contextOptions: ContextOptions) { + for(key, val) in dict { + let keypath = prefix == "" ? key : prefix + "." + key + if val is [String: Any] { + mergeFields(tableName: tableName, prefix: keypath, dict: val as! [String: Any], contextOptions: contextOptions) + } else { + if mergedDict[keyPath: keypath] == nil { + mergedDict[keyPath: keypath] = val + } else { + if !self.breakFlag { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: key).getErrorObject(contextOptions: contextOptions)) + self.breakFlag = true + return + } + } + } + } + } + + internal static func createRequestBody( + elements: [TextField], + additionalFields: AdditionalFields? = nil, + callback: Callback, + contextOptions: ContextOptions + ) -> [String: Any]? { + var tableMap: [String: Int] = [:] + var payload: [[String: Any]] = [] + var updatePayload: [String: Any] = [:] + self.callback = callback + self.breakFlag = false + self.tableSet = Set() + var index: Int = 0 + + if let additionalFields = additionalFields { + for (recordIndex, entry) in additionalFields.records.enumerated() { + let tableName = entry.tableName + let fields = entry.data + // An explicit empty-string skyflowId is a caller bug — reject it rather than + // silently inserting a new row where an update was intended. + if let skyflowId = entry.skyflowId, skyflowId.isEmpty { + self.callback?.onFailure(ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index \(recordIndex)").getErrorObject(contextOptions: contextOptions)) + return nil + } + if let skyflowId = entry.skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + let temp = updatePayload[skyflowId] as! [String: Any] + var existingFields = temp["fields"] as! [String: Any] + for (key, val) in fields { + existingFields[key] = val + } + var updatedTemp = temp + updatedTemp["fields"] = existingFields + updatePayload[skyflowId] = updatedTemp + } else { + let temp: [String: Any] = [ + "table": tableName, + "fields": fields, + "skyflowId": skyflowId + ] + updatePayload[skyflowId] = temp + } + continue + } + if tableMap[tableName] != nil { + let inputEntry = payload[tableMap[tableName]!] + mergedDict = inputEntry["fields"] as! [String: Any] + self.mergeFields(tableName: tableName, prefix: "", dict: fields, contextOptions: contextOptions) + if self.breakFlag { + return nil + } + payload[tableMap[tableName]!]["fields"] = mergedDict + mergedDict = [:] + } else { + tableMap[tableName] = index + let temp: [String: Any] = [ + "table": tableName, + "fields": fields + ] + self.addFieldsToTableSet(tableName: tableName, prefix: "", fields: fields, contextOptions: contextOptions) + if self.breakFlag { + return nil + } + payload.append(temp) + index += 1 + } + } + } + + for element in elements { + let tableName = element.tableName! + let columnName = element.columnName! + let value = element.getValue() + let skyflowId = element.skyflowId // Assumes TextField has this property + + // Same guard as additionalFields above: CollectElementInput now defaults + // skyflowId to nil, so an empty string can only be an explicit caller mistake. + if let skyflowId = skyflowId, skyflowId.isEmpty { + self.callback?.onFailure(ErrorCodes.EMPTY_SKYFLOW_ID(value: "element with column '\(columnName)'").getErrorObject(contextOptions: contextOptions)) + return nil + } + if let skyflowId = skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + var temp = updatePayload[skyflowId] as! [String: Any] + var existingFields = temp["fields"] as! [String: Any] + if existingFields[columnName] != nil { + var hasElementValueMatchRule: Bool = false + for validation in element.userValidationRules.rules { + if validation is ElementValueMatchRule { + hasElementValueMatchRule = true + break; + } + } + if(!hasElementValueMatchRule) + { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: [ element.columnName, element.tableName!]).getErrorObject(contextOptions: contextOptions)) + return nil + } + continue; + } else { + existingFields[columnName] = value + } + temp["fields"] = existingFields + updatePayload[skyflowId] = temp + } else { + let temp: [String: Any] = [ + "table": tableName, + "fields": [columnName: value], + "skyflowId": skyflowId + ] + updatePayload[skyflowId] = temp + } + } else { + // Only add to payload + if tableMap[(element.tableName)!] != nil { + var temp = payload[tableMap[(element.tableName)!]!] + temp[keyPath: "fields." + (element.columnName)!] = element.getValue() + let tableSetEntry = element.tableName! + "-" + element.columnName + if tableSet.contains(tableSetEntry) { + var hasElementValueMatchRule: Bool = false + for validation in element.userValidationRules.rules { + if validation is ElementValueMatchRule { + hasElementValueMatchRule = true + break; + } + } + if(!hasElementValueMatchRule) + { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: [ element.columnName, element.tableName!]).getErrorObject(contextOptions: contextOptions)) + return nil + } + continue; + } + self.tableSet.insert(tableSetEntry) + payload[tableMap[(element.tableName)!]!] = temp + } else { + tableMap[(element.tableName)!] = index + index += 1 + var temp: [String: Any] = [ + "table": element.tableName!, + "fields": [:] + ] + temp[keyPath: "fields." + element.columnName!] = element.getValue() + self.tableSet.insert(element.tableName! + "-" + element.columnName) + payload.append(temp) + } + } + } + return ["records": payload, "update": updatePayload] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultICOptions.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultICOptions.swift new file mode 100644 index 00000000..f72f47d9 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultICOptions.swift @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Options passed internally for FlowDB v2 collect/insert calls + +import Foundation + +internal struct FlowVaultICOptions { + var additionalFields: AdditionalFields? + var upsert: [UpsertOption]? + var callback: Callback? + var contextOptions: ContextOptions? + + init(additionalFields: AdditionalFields? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { + self.additionalFields = additionalFields + self.upsert = upsert + self.callback = callback + self.contextOptions = contextOptions + } + + public func validateUpsert() -> Bool{ + if self.upsert != nil { + if self.upsert!.count == 0 { + let errorCode = ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY() + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + + for (index, currUpsertOption) in self.upsert!.enumerated() { + if currUpsertOption.tableName == "" { + let errorCode = ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + if currUpsertOption.uniqueColumns.isEmpty { + let errorCode = ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + } + + } + return false; + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift new file mode 100644 index 00000000..22a99ac2 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Callback used while API callback for Collect the elements (FlowDB v2) + +import Foundation +import UIKit + +internal class FlowVaultInsertAPICallback: Callback { + // Overridable only for tests (e.g. injecting a URLProtocol mock via protocolClasses) - global + // URLProtocol.registerClass(_:) isn't reliably consulted for manually-created URLSession + // instances in every environment, so tests need a real seam here instead. + internal static var urlSessionConfiguration: URLSessionConfiguration = .default + var apiClient: APIClient + var records: [String: Any] + var callback: Callback + var options: FlowVaultICOptions + var contextOptions: ContextOptions + + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + self.records = records + self.apiClient = apiClient + self.callback = callback + self.options = options + self.contextOptions = contextOptions + } + internal func onSuccess(_ responseBody: Any) { + let insertRecords = records["records"] as? [[String: Any]] ?? [] + let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) + let hasInsert = !insertRecords.isEmpty + let hasUpdate = !updateRecords.isEmpty + + if !hasInsert && !hasUpdate { + self.callback.onSuccess(["records": []]) + return + } + + // No update records: preserve the original single-call insert behavior exactly + // (including throwing a full-request-level failure straight to onFailure). + if !hasUpdate { + guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + self.callback.onFailure(response) + return + } + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) + } catch { + self.callback.onFailure(error) + } + } + task.resume() + } catch let error { + self.callback.onFailure(error) + } + return + } + + guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + + let group = DispatchGroup() + var mergedRecords: [[String: Any]] = [] + var mergedErrors: [[String: Any]] = [] + + if hasInsert { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + if hasUpdate { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! + do { + let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + group.notify(queue: .main) { + if mergedErrors.isEmpty { + self.callback.onSuccess(["records": mergedRecords]) + } else { + self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) + } + } + } + + internal func onFailure(_ error: Any) { + self.callback.onFailure(error) + } + + internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { + var updateRecords: [[String: Any]] = [] + for (skyflowID, value) in updateDict { + guard let entry = value as? [String: Any], + let tableName = entry["table"] as? String, + let fields = entry["fields"] as? [String: Any] else { continue } + updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) + } + return updateRecords + } + + internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { + var temp: [String: Any] = [:] + for (key, val) in dict { + if let v = val as? [String: Any] { + temp[key] = buildFieldsDict(dict: v) + } else { + temp[key] = val + } + } + return temp + } + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: Self.urlSessionConfiguration)) + + } + + internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: Self.urlSessionConfiguration)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every record in the batch fails, + // but the body still has the same {"records": [...]} shape as a success/partial + // response (each record carrying its own error/httpCode) - parse it as such. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["records"] != nil { + return try getCollectResponseBody(data: safeData) + } + var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] + } + // Pass the vault's structured error object (grpcCode/httpStatus/details) through + // as-is, only patching in the request-id and defaulting httpCode when absent. + if var errorDict = errorResponse["error"] as? [String: Any] { + if let message = errorDict["message"] as? String { + description = message + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorDict["message"] = description + } + if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } + return ["error": errorDict] + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } + return ["error": ["message": description, "httpCode": httpResponse.statusCode]] + } + } + + guard let safeData = data else { + return ["records": []] + } + + return try getCollectResponseBody(data: safeData) + + } + + func getCollectResponseBody(data: Data) throws -> [String: Any]{ + let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] + var records: [[String: Any]] = [] + + let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + records.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } + var fields: [String: Any] = [:] + if let tokens = entry["tokens"] as? [String: Any] { + for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { + fields[column] = tokenValue + } + } + successEntry["fields"] = fields + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + records.append(successEntry) + } + } + + return ["records": records] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift new file mode 100644 index 00000000..0c35872e --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 insert api call + +import Foundation + +internal class FlowVaultInsertRequestBody { + internal static func createRequestBody(vaultID: String, records: [String: Any], options: FlowVaultICOptions) -> [String: Any] { + var recordsPayload: [[String: Any]] = [] + for record in (records["records"] as! [[String: Any]]) { + var temp: [String: Any] = [:] + temp["data"] = record["fields"] + if let tableName = record["table"] as? String { + temp["tableName"] = tableName + if let upsertOptions = options.upsert, let match = upsertOptions.first(where: { $0.tableName == tableName }) { + var upsertPayload: [String: Any] = ["uniqueColumns": match.uniqueColumns] + if let updateType = match.updateType { + upsertPayload["updateType"] = updateType.rawValue + } + temp["upsert"] = upsertPayload + } + } + recordsPayload.append(temp) + } + return ["vaultID": vaultID, "records": recordsPayload] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift new file mode 100644 index 00000000..54187810 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 update-by-skyflowID api call + +import Foundation + +internal class FlowVaultUpdateRequestBody { + internal static func createRequestBody(vaultID: String, records: [[String: Any]]) -> [String: Any] { + return ["vaultID": vaultID, "records": records] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/InsertOptions.swift b/Sources/SkyflowFlowVaultIOS/collect/InsertOptions.swift new file mode 100644 index 00000000..9f4f4301 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/InsertOptions.swift @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes the Options for Insert + +import Foundation + +public struct InsertOptions { + var upsert: [UpsertOption]? + public init(upsert: [UpsertOption]? = nil) { + self.upsert = upsert + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/UpsertOption.swift b/Sources/SkyflowFlowVaultIOS/collect/UpsertOption.swift new file mode 100644 index 00000000..63d2d4db --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/collect/UpsertOption.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes an upsert configuration for insert/collect calls + +import Foundation + +public enum UpdateType: String { + case UPDATE + case REPLACE +} + +public struct UpsertOption { + public let tableName: String + public let uniqueColumns: [String] + public let updateType: UpdateType? + + public init(tableName: String, uniqueColumns: [String], updateType: UpdateType? = nil) { + self.tableName = tableName + self.uniqueColumns = uniqueColumns + self.updateType = updateType + } +} diff --git a/Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift b/Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift new file mode 100644 index 00000000..9ccbb1be --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// FlowVault (v2) contract operations on the shared APIClient. + +import Foundation + +extension APIClient { + internal func postAndUpdate(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { + let collectApiCallback = FlowVaultCollectAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + self.getAccessToken(callback: collectApiCallback, contextOptions: contextOptions) + } + internal func post(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { + let insertApiCallback = FlowVaultInsertAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + self.getAccessToken(callback: insertApiCallback, contextOptions: contextOptions) + } + + internal func get(records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil, callback: Callback, contextOptions: ContextOptions) { + let revealApiCallback = FlowVaultRevealAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v2/tokens/detokenize"), records: records, tokenGroupRedactions: tokenGroupRedactions, contextOptions: contextOptions) + self.getAccessToken(callback: revealApiCallback, contextOptions: contextOptions) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/core/Client.swift b/Sources/SkyflowFlowVaultIOS/core/Client.swift new file mode 100644 index 00000000..94de1c23 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/core/Client.swift @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Implementation of the Skyflow Client class (FlowVault v2 contract). +// The v2 operations live in ClientOperations.swift as an extension of this +// class. Future enhancements specific to this SDK go here (or in extensions +// in this target) without affecting the legacy Skyflow SDK. + +import Foundation + +public class Client: ClientBase { + public init(_ skyflowConfig: Configuration) { + super.init(skyflowConfig.data) + } +} + +public extension Client { + func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { + return makeContainer(type: type, options: options?.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift b/Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift new file mode 100644 index 00000000..4e69ead1 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// FlowVault (v2) contract operations on this SDK's Client class (declared in Client.swift). + +import Foundation + +extension Client { + internal func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .INSERT + Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) + if self.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + let icOptions = FlowVaultICOptions(upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) + var errorCode: ErrorCodes? + + if records["records"] == nil { + errorCode = .RECORDS_KEY_ERROR() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) + if let recordEntries = records["records"] as? [[String: Any]] { + for (index, record) in recordEntries.enumerated() { + if record["table"] != nil { + if !(record["table"] is String) { + errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } else { + if (record["table"] as! String).isEmpty { + errorCode = .EMPTY_TABLE_NAME() + } else { + if record["fields"] != nil { + if !(record["fields"] is [String: Any]) { + errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") + break + } + let fields = record["fields"] as! [String: Any] + if fields.isEmpty { + errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") + } + } else { + errorCode = .FIELDS_KEY_ERROR(value: "\(index)") + } + } + } + } else { + errorCode = .TABLE_KEY_ERROR(value: "\(index)") + } + } + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } else { + if options.upsert != nil { + if icOptions.validateUpsert() { + return; + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.post(records: records, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) + } + } else { + errorCode = .INVALID_RECORDS_TYPE() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + + internal func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .DETOKENIZE + func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { + if token["token"] == nil { + return .ID_KEY_ERROR() + } else { + guard let _ = token["token"] as? String else { + return .INVALID_TOKEN_TYPE(value: "\(index)") + } + } + return nil + } + + Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) + if self.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) + } + + if let tokens = records["records"] as? [[String: Any]] { + var list: [RevealRequestRecord] = [] + if tokens.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index,token) in tokens.enumerated() { + let errorCode = checkRecord(token: token, index: index) + if errorCode == nil, let id = token["token"] as? String { + list.append(RevealRequestRecord(token: id)) + } else { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + +} diff --git a/Sources/SkyflowFlowVaultIOS/core/Configuration.swift b/Sources/SkyflowFlowVaultIOS/core/Configuration.swift new file mode 100644 index 00000000..df1e4992 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/core/Configuration.swift @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Configure Skyflow, implementation for the Configuration object. +// Contract-identical in both SDKs today; declared per SDK so either product +// can add configuration options independently in the future. + +import Foundation + +public struct Configuration { + package var data: BaseConfiguration + + public init(vaultID: String = "", vaultURL: String = "", tokenProvider: TokenProvider, options: Options? = Options()) { + self.data = BaseConfiguration(vaultID: vaultID, vaultURL: vaultURL, tokenProvider: tokenProvider, options: options) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/core/ContainerOptions.swift b/Sources/SkyflowFlowVaultIOS/core/ContainerOptions.swift new file mode 100644 index 00000000..5aeea0b2 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/core/ContainerOptions.swift @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Options for composable containers (layout/styles). Contract-identical in +// both SDKs today; declared per SDK so either product can evolve +// independently. + +import Foundation + +public struct ContainerOptions { + package var data: BaseContainerOptions // make it naming with prefix Base + + public init(){ + self.data = BaseContainerOptions() + } + + public init(layout: [Int], styles: Styles? = Styles(), errorTextStyles: Styles? = Styles()) { + self.data = BaseContainerOptions(layout: layout, styles: styles, errorTextStyles: errorTextStyles) + } + +} diff --git a/Sources/SkyflowFlowVaultIOS/elements/CollectElementInput.swift b/Sources/SkyflowFlowVaultIOS/elements/CollectElementInput.swift new file mode 100644 index 00000000..31fba2d9 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/elements/CollectElementInput.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// An Object that describes SkyflowInputField (FlowVault contract: +// `tableName:` and `skyflowId:` argument labels). A struct: elements store +// their own copy, so reusing one input never shares state. + +import Foundation + +public struct CollectElementInput { + package var data: BaseCollectElementInput + + public init(tableName: String = "", column: String = "", + inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", + placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowId: String? = nil) { + self.data = BaseCollectElementInput(tableName: tableName, column: column, inputStyles: inputStyles!, labelStyles: labelStyles!, errorTextStyles: errorTextStyles!, iconStyles: iconStyles!, label: label!, placeholder: placeholder!, type: nil, validations: validations, skyflowId: skyflowId) + } + + public init(tableName: String = "", column: String = "", + inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", + placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowId: String? = nil) { + self.data = BaseCollectElementInput(tableName: tableName, column: column, inputStyles: inputStyles!, labelStyles: labelStyles!, errorTextStyles: errorTextStyles!, iconStyles: iconStyles!, label: label!, placeholder: placeholder!, type: type, validations: validations, skyflowId: skyflowId) + } + + @available(*, deprecated, message: "altText param is deprecated") + public init(tableName: String = "", column: String = "", + inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", + placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowId: String? = nil) { + self.data = BaseCollectElementInput(tableName: tableName, column: column, inputStyles: inputStyles!, labelStyles: labelStyles!, errorTextStyles: errorTextStyles!, iconStyles: iconStyles!, label: label!, placeholder: placeholder!, type: type, validations: validations, skyflowId: skyflowId) + } +} + +public extension TextField { + func update(update: CollectElementInput) { + self.update(data: update.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/elements/CollectElementOptions.swift b/Sources/SkyflowFlowVaultIOS/elements/CollectElementOptions.swift new file mode 100644 index 00000000..13f18716 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/elements/CollectElementOptions.swift @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// An Object that describes Options parameter for SkyflowInputField. +// Contract-identical in both SDKs today; declared per SDK so either product +// can add or remove options independently in the future. + +import Foundation +#if os(iOS) +import UIKit +#endif + +public struct CollectElementOptions { + package var data: BaseCollectElementOptions + + public init(required: Bool? = false, enableCardIcon: Bool = true, format: String = "mm/yy", translation: [ Character: String ]? = nil, enableCopy: Bool = false, cardMetaData: [ String: Any]? = nil) { + self.data = BaseCollectElementOptions(required: required!, enableCardIcon: enableCardIcon, format: format, translation: translation, enableCopy: enableCopy, cardMetaData: cardMetaData) + } +} + +public extension TextField { + func update(updateOptions: CollectElementOptions) { + self.update(optionsData: updateOptions.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift b/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift new file mode 100644 index 00000000..0a689f8d --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 detokenize api call + +import Foundation + +internal class FlowVaultDetokenizeRequestBody { + internal static func createRequestBody(vaultID: String, records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil) -> [String: Any] { + let tokens = records.map { $0.token } + var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] + if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { + // Pass through exactly what was collected from the mounted elements, duplicates + // and all - the API validates tokenGroupRedactions, not the SDK. + body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } + } + return body + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift b/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift new file mode 100644 index 00000000..7932295e --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Implementation of callback for Reveal api (FlowDB v2) + +import Foundation + +class FlowVaultRevealAPICallback: Callback { + var apiClient: APIClient + var callback: Callback + var connectionUrl: String + var records: [RevealRequestRecord] + var tokenGroupRedactions: [TokenGroupRedaction]? + var contextOptions: ContextOptions + + + internal init(callback: Callback, apiClient: APIClient, connectionUrl: String, + records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil, contextOptions: ContextOptions) { + self.apiClient = apiClient + self.callback = callback + self.connectionUrl = connectionUrl + self.records = records + self.tokenGroupRedactions = tokenGroupRedactions + self.contextOptions = contextOptions + } + + internal func onSuccess(_ token: Any) { + guard let url = URL(string: connectionUrl) else { + let errorCode = ErrorCodes.INVALID_URL() + self.callRevealOnFailure(callback: self.callback, errorObject: errorCode.getErrorObject(contextOptions: self.contextOptions)) + return + } + + do { + let (request, session) = try getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + self.callback.onSuccess(response) + } catch { + self.callRevealOnFailure(callback: self.callback, errorObject: error) + } + } + task.resume() + } catch let error { + self.callRevealOnFailure(callback: self.callback, errorObject: error) + } + } + + internal func onFailure(_ error: Any) { + if error is Error { + callRevealOnFailure(callback: self.callback, errorObject: error as! Error) + } else { + self.callback.onFailure(error) + } + } + + private func callRevealOnFailure(callback: Callback, errorObject: Error) { + let result = ["errors": [["error": errorObject]]] + callback.onFailure(result) + } + + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records, tokenGroupRedactions: tokenGroupRedactions)) + request.httpBody = data + } + + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + throw error! + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every token in the batch fails to + // detokenize, but the body still has the same {"response": [...]} shape as a + // success/partial response (each entry carrying its own error/httpCode) - parse + // it as such instead of collapsing into a generic top-level error. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["response"] != nil { + return try getDetokenizeResponseBody(data: safeData) + } + var description = "Detokenize call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + do { + let errorResponse = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] + if let errorDetails = errorResponse["error"] as? [String: Any], + let message = errorDetails["message"] as? String { + description = message + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } catch { + throw ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8) ?? "Unknown error").getErrorObject(contextOptions: self.contextOptions) + } + } + throw ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) + } + } + + guard let safeData = data else { + return ["records": []] + } + + return try getDetokenizeResponseBody(data: safeData) + } + + func getDetokenizeResponseBody(data: Data) throws -> [String: Any] { + let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + var records: [[String: Any]] = [] + + let responseRecords = jsonData["response"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let token = entry["token"] { errorEntry["token"] = token } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + records.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let token = entry["token"] { successEntry["token"] = token } + if let value = entry["value"] { successEntry["value"] = value } + if let tokenGroupName = entry["tokenGroupName"] { successEntry["tokenGroupName"] = tokenGroupName } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + if let metadata = entry["metadata"] as? [String: Any] { successEntry["metadata"] = metadata } + records.append(successEntry) + } + } + + return ["records": records] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift new file mode 100644 index 00000000..269cade6 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// FlowVault implementation of the reveal() operation (v2 detokenize with +// token-group redactions). + +import Foundation + +public extension Container { + func reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { + var tempContextOptions = self.skyflow.contextOptions + tempContextOptions.interface = .REVEAL_CONTAINER + if self.skyflow.vaultID.isEmpty { + let errorCode = ErrorCodes.EMPTY_VAULT_ID() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + if self.skyflow.vaultURL == "/" { + let errorCode = ErrorCodes.EMPTY_VAULT_URL() + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + var errorCode: ErrorCodes? + Log.info(message: .VALIDATE_REVEAL_RECORDS, contextOptions: tempContextOptions) + if let element = ConversionHelpers.checkElementsAreMounted(elements: self.revealElements) as? Label { + errorCode = .UNMOUNTED_REVEAL_ELEMENT(value: element.revealInput.token) + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + for element in self.revealElements { + if element.errorTriggered { + errorCode = .ERROR_TRIGGERED(value: element.triggeredErrorMessage) + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + if element.getToken().isEmpty { + errorCode = .EMPTY_TOKEN_ID() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + } + // Mirrors the JS SDK's validateRevealOptions: every redaction entry must have a + // non-empty tokenGroupName and redaction. Whitespace-only values are rejected + // too — stricter than JS, which only checks !== ''. (The JS array/string type + // checks are enforced by Swift's type system and don't need porting.) + if let redactions = options?.tokenGroupRedactions { + for (index, entry) in redactions.enumerated() { + if entry.tokenGroupName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + || entry.redaction.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + errorCode = .INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: String(index)) + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + } + } + let revealValueCallback = RevealValueCallback(callback: callback, revealElements: self.revealElements, contextOptions: tempContextOptions) + let records = RevealRequestBody.createRequestBody(elements: self.revealElements) + + if let tokens = records["records"] as? [[String: Any]] { + var list: [RevealRequestRecord] = [] + for token in tokens { + if let id = token["token"] as? String { + list.append(RevealRequestRecord(token: id)) + } + } + let logCallback = LogCallback(clientCallback: revealValueCallback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .REVEAL_SUBMIT_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.skyflow.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) + } + } +} + +public extension Container { + func create(input: RevealElementInput, options: RevealElementOptions? = RevealElementOptions()) -> Label where T: RevealContainer { + return makeRevealElement(input: input.data, options: options?.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealElementInput.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealElementInput.swift new file mode 100644 index 00000000..6c0788cf --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealElementInput.swift @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object for reveal element (FlowVault contract: no per-token redaction; +// redaction is controlled via tokenGroupRedactions on reveal()). A struct: +// elements store their own copy, so reusing one input never shares state. + +import Foundation + +public struct RevealElementInput { + package var data: BaseRevealElementInput + + public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "") { + self.data = BaseRevealElementInput(token: token, inputStyles: inputStyles, labelStyles: labelStyles, errorTextStyles: errorTextStyles, label: label, redaction: .PLAIN_TEXT, altText: altText) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealElementOptions.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealElementOptions.swift new file mode 100644 index 00000000..959b537c --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealElementOptions.swift @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Options for reveal elements. Contract-identical in both SDKs today; +// declared per SDK so either product can evolve independently. + +import Foundation + +public struct RevealElementOptions { + package var data: BaseRevealElementOptions + + public init(format: String? = nil, translation: [ Character: String ]? = nil, enableCopy: Bool? = false) { + self.data = BaseRevealElementOptions(format: format, translation: translation, enableCopy: enableCopy) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealOptions.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealOptions.swift new file mode 100644 index 00000000..eaf1de74 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealOptions.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// +// File.swift +// +// +// Created by Akhil Anil Mangala on 11/08/21. +// + +import Foundation + +public struct RevealOptions { + public let tokenGroupRedactions: [TokenGroupRedaction]? + + public init(tokenGroupRedactions: [TokenGroupRedaction]? = nil) { + self.tokenGroupRedactions = tokenGroupRedactions + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift new file mode 100644 index 00000000..8b0230e1 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for reveal api call + +import Foundation + +internal class RevealRequestBody { + internal static func createRequestBody(elements: [Label]) -> [String: Any] { + var payload: [[String: Any]] = [] + for element in elements { + var entry: [String: Any] = [:] + entry["token"] = element.revealInput.token + payload.append(entry) + } + + return ["records": payload] + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestRecord.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestRecord.swift new file mode 100644 index 00000000..2652b247 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestRecord.swift @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes the reveal record in request body + +import Foundation + +struct RevealRequestRecord { + var token: String + init(token: String) { + self.token = token + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealResponse.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealResponse.swift new file mode 100644 index 00000000..f9baa0c6 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealResponse.swift @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Public typed wrappers around the dictionary delivered via Callback.onSuccess for both +// Client.detokenize() and Container.reveal() - both deliver the same merged +// {"records": [...]} shape (success and per-token failures share one array, matching Collect's +// convention). The Callback protocol still delivers raw [String: Any] as before - these types +// don't change that, they just wrap it directly (no JSONDecoder round-trip). Usage: +// +// public func onSuccess(_ responseBody: Any) { +// guard let response = Skyflow.RevealResponse(responseBody) else { return } +// ... +// } + +import Foundation + +public struct RevealResponse { + public let records: [RevealRecord] + + public init?(_ responseBody: Any) { + guard let dict = responseBody as? [String: Any], + let recordDicts = dict["records"] as? [[String: Any]] else { return nil } + self.records = recordDicts.map { RevealRecord($0) } + } +} + +// Each entry in "records" is either a successfully revealed token (error is nil) or a failed one +// (error is non-nil) - the vault returns both together in the same array, each tagged with its +// own httpCode. +public struct RevealRecord { + public let token: String? + public let tokenGroupName: String? + // Keyed by whatever fields the vault includes (e.g. "skyflowId", "tableName"). + public let metadata: [String: Any]? + public let httpCode: Int + public let error: String? + + init(_ dict: [String: Any]) { + self.token = dict["token"] as? String + self.tokenGroupName = dict["tokenGroupName"] as? String + self.metadata = RevealRecord.normalizeMetadataKeys(dict["metadata"] as? [String: Any]) + self.httpCode = dict["httpCode"] as? Int ?? 0 + self.error = dict["error"] as? String + } + + // The vault sends this key as "skyflowID" - normalized here to "skyflowId" so metadata's + // casing is consistent with Skyflow.CollectRecord.skyflowId rather than exposing the raw + // wire casing directly. + private static func normalizeMetadataKeys(_ metadata: [String: Any]?) -> [String: Any]? { + guard let metadata = metadata else { return nil } + var normalized: [String: Any] = [:] + for (key, value) in metadata { + normalized[key == "skyflowID" ? "skyflowId" : key] = value + } + return normalized + } +} + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into RevealResponse +// for you. Use it directly with the callback-based reveal(callback:options:) - RevealValueCallback +// merges success/failure into one "records" array (matching Collect's convention), so the same +// RevealResponse type used for Client.detokenize() applies here too. +public class RevealCallback: Callback { + private let successHandler: (RevealResponse) -> Void + private let failureHandler: (SkyflowError) -> Void + + public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = RevealResponse(responseBody) else { + failureHandler(SkyflowError.wrap(responseBody)) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-token failure inside RevealResponse.records, and for client-side validation + // failures (empty vaultID, unmounted element, etc.). Always normalized into a + // Skyflow.SkyflowError - see SkyflowError.wrap. + public func onFailure(_ error: Any) { + failureHandler(SkyflowError.wrap(error)) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealValueCallback.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealValueCallback.swift new file mode 100644 index 00000000..4c7e1d2b --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/RevealValueCallback.swift @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +import Foundation + +internal class RevealValueCallback: Callback { + var clientCallback: Callback + var revealElements: [Label] + var contextOptions: ContextOptions + + internal init(callback: Callback, revealElements: [Label], contextOptions: ContextOptions) { + self.clientCallback = callback + self.revealElements = revealElements + self.contextOptions = contextOptions + } + + func onSuccess(_ responseBody: Any) { + var tokens: [String: String] = [:] + + let responseJson = responseBody as? [String: Any] ?? [:] + var response: [String: Any] = [:] + var records: [[String: Any]] = [] + var errors: [[String: Any]] = [] + + // Success and per-token failure entries arrive together in one "records" array (matching + // Collect's convention and the JS SDK's shape), each distinguished by an "error" key. + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { + guard let dict = record as? [String: Any] else { continue } + if dict["error"] != nil { + errors.append(dict) + records.append(dict) + continue + } + guard let token = dict["token"] as? String else { continue } + let value = dict["value"] as? String + tokens[token] = value ?? token + + records.append(dict) + } + } + + response["records"] = records + let tokensToErrors = getTokensToErrors(errors) + + DispatchQueue.main.async { + for revealElement in self.revealElements { + if let v = tokens[revealElement.revealInput.token]{ + revealElement.updateVal(value: v) + } + + let inputToken = revealElement.revealInput.token + revealElement.hideError() + + if let errorMessage = tokensToErrors[inputToken] { + revealElement.showError(message: errorMessage) + } else { + Log.info(message: .ELEMENT_REVEALED, values: [revealElement.revealInput.label], contextOptions: self.contextOptions) + } + } + } + self.clientCallback.onSuccess(response) + } + + // Reserved for whole-request failures: a genuinely opaque error (can't be parsed as a + // dictionary at all), or a network/API-level error not scoped to any specific token (no + // "token" key - e.g. an invalid bearer token, a connection failure). Only PER-TOKEN failures + // (each tagged with the "token" they belong to) are routed through onSuccess instead, matching + // Collect's convention of surfacing per-record issues via the record's own "error" field. + func onFailure(_ error: Any) { + guard let responseJson = error as? [String: Any] else { + self.clientCallback.onFailure(error) + return + } + + var errors = [] as [[String: Any]] + if let responseErrors = responseJson["errors"] as? [[String: Any]] { + errors = responseErrors + } + + // Not scoped to any specific reveal element - a genuine network/API-level failure, not a + // per-record issue, so it's delivered via onFailure rather than folded into the records array. + if let networkError = errors.first(where: { $0["token"] == nil }) { + DispatchQueue.main.async { + for revealElement in self.revealElements { + revealElement.hideError() + } + } + self.clientCallback.onFailure(networkError["error"] ?? error) + return + } + + var tokens: [String: String] = [:] + var records: [[String: Any]] = [] + + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { + guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } + let value = dict["value"] as? String + tokens[token] = value ?? token + + records.append(dict) + } + } + + // Each entry's "error" arrives as an NSError here (built internally, not deserialized from + // JSON) - convert to a plain message string so it matches the "records" array's normal + // shape (each entry's "error" is always a String, whether from onSuccess or here). + let stringifiedErrors = errors.map { entry -> [String: Any] in + var updated = entry + if let nsError = entry["error"] as? NSError { + updated["error"] = nsError.localizedDescription + } + return updated + } + records.append(contentsOf: stringifiedErrors) + + let tokensToErrors = getTokensToErrors(errors) + + DispatchQueue.main.async { + for revealElement in self.revealElements { + if let v = tokens[revealElement.revealInput.token]{ + revealElement.updateVal(value: v) + } + + let inputToken = revealElement.revealInput.token + revealElement.hideError() + if let errorMessage = tokensToErrors[inputToken] { + revealElement.showError(message: errorMessage) + } + } + } + self.clientCallback.onSuccess(["records": records]) + } + + func getTokensToErrors(_ errors: [[String: Any]]?) -> [String: String] { + var result = [String: String]() + if let errorsObj = errors { + for error in errorsObj { + if let token = error["token"] as? String { + result[token] = "Invalid Token" + } + } + } + return result + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/TokenGroupRedaction.swift b/Sources/SkyflowFlowVaultIOS/reveal/TokenGroupRedaction.swift new file mode 100644 index 00000000..c5b3cd51 --- /dev/null +++ b/Sources/SkyflowFlowVaultIOS/reveal/TokenGroupRedaction.swift @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes a request-level, per-token-group redaction for detokenize + +import Foundation + +public struct TokenGroupRedaction { + public let tokenGroupName: String + public let redaction: String + + public init(tokenGroupName: String, redaction: String) { + self.tokenGroupName = tokenGroupName + self.redaction = redaction + } +} diff --git a/Tests/skyflow-iOS-collectTests/DemoImpl.swift b/Tests/skyflow-iOS-collectTests/DemoImpl.swift index 7d3e3f85..094be014 100644 --- a/Tests/skyflow-iOS-collectTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-collectTests/DemoImpl.swift @@ -11,7 +11,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { @@ -42,12 +43,24 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "default" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: CollectResponse? + var revealResponse: RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? CollectResponse { + self.collectResponse = response + expectation.fulfill() + return + } + if let response = responseBody as? RevealResponse { + self.revealResponse = response + expectation.fulfill() + return + } do { let dataString = String(data: try JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) if let unwrapped = dataString { @@ -68,4 +81,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: CollectCallback { + CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: RevealCallback { + RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index 78e42f2c..cdee80fc 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -3,14 +3,15 @@ */ import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class skyflow_iOS_collectTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = Skyflow.initialize( + self.skyflow = SkyflowFlowVaultIOS.initialize( Configuration( vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, @@ -33,7 +34,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -57,7 +58,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -85,7 +86,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -107,7 +108,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -127,7 +128,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -144,7 +145,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -160,7 +161,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -177,7 +178,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) _ = container?.create(input: collectInput, options: options) @@ -193,21 +194,21 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let collectElement = container?.create(input: collectInput, options: options) - collectElement?.on(eventName: Skyflow.EventName.CHANGE) { state in + collectElement?.on(eventName: EventName.CHANGE) { state in print("state", state) } - collectElement?.on(eventName: Skyflow.EventName.BLUR) { state in + collectElement?.on(eventName: EventName.BLUR) { state in print("state", state) } - collectElement?.on(eventName: Skyflow.EventName.FOCUS) { state in + collectElement?.on(eventName: EventName.FOCUS) { state in print("state", state) } - collectElement?.on(eventName: Skyflow.EventName.READY) { _ in + collectElement?.on(eventName: EventName.READY) { _ in onReadyCalled = true } sleep(1) @@ -225,7 +226,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -237,7 +238,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -254,7 +255,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", label: "Card Number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", label: "Card Number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -264,7 +265,7 @@ final class skyflow_iOS_collectTests: XCTestCase { window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) @@ -277,7 +278,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -294,7 +295,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -302,7 +303,7 @@ final class skyflow_iOS_collectTests: XCTestCase { window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) @@ -314,7 +315,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -329,7 +330,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: true) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -339,7 +340,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -386,7 +387,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert with invalid token") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: ["records": records], options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: ["records": records], options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) @@ -405,7 +406,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -428,7 +429,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let mycontainer = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) let textField = mycontainer?.create(input: collectInput) let window = UIWindow() @@ -439,7 +440,7 @@ final class skyflow_iOS_collectTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber INVALID_CARD_NUMBER\n") @@ -451,7 +452,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let mycontainer = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) let textField = mycontainer?.create(input: collectInput) let window = UIWindow() @@ -462,7 +463,7 @@ final class skyflow_iOS_collectTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber Regex match failed\n") @@ -474,7 +475,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let collectOptions = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "pin", placeholder: "pin", type: .PIN) + let collectInput = CollectElementInput(tableName: "persons", column: "pin", placeholder: "pin", type: .PIN) let pinElement = container?.create(input: collectInput, options: collectOptions) @@ -500,7 +501,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let mycontainer = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) let textField = mycontainer?.create(input: collectInput) let window = UIWindow() @@ -513,7 +514,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber triggered error\n") @@ -523,14 +524,14 @@ final class skyflow_iOS_collectTests: XCTestCase { let collectOptions = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "pin", placeholder: "pin", type: .PIN) + let collectInput = CollectElementInput(tableName: "persons", column: "pin", placeholder: "pin", type: .PIN) let pinElement = container?.create(input: collectInput, options: collectOptions) var vs = ValidationSet() vs.add(rule: ElementValueMatchRule(element: pinElement!, error: "ELEMENT NOT MATCHING")) - let collectInput2 = CollectElementInput(table: "persons", column: "", placeholder: "pin", type: .PIN, validations: vs) + let collectInput2 = CollectElementInput(tableName: "persons", column: "", placeholder: "pin", type: .PIN, validations: vs) let confirmPinElement = container?.create(input: collectInput2, options: collectOptions) @@ -554,14 +555,14 @@ final class skyflow_iOS_collectTests: XCTestCase { let collectOptions = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "pin", placeholder: "pin", type: .PIN) + let collectInput = CollectElementInput(tableName: "persons", column: "pin", placeholder: "pin", type: .PIN) let pinElement = container?.create(input: collectInput, options: collectOptions) var vs = ValidationSet() vs.add(rule: ElementValueMatchRule(element: pinElement!, error: "ELEMENT NOT MATCHING")) - let collectInput2 = CollectElementInput(table: "persons", column: "pin", placeholder: "pin", type: .PIN, validations: vs) + let collectInput2 = CollectElementInput(tableName: "persons", column: "pin", placeholder: "pin", type: .PIN, validations: vs) let confirmPinElement = container?.create(input: collectInput2, options: collectOptions) @@ -579,7 +580,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectSuccess = XCTestExpectation(description: "Should succeed") let myCallback = DemoAPICallback(expectation: expectSuccess) - let records = CollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) let recordElement = (records?["records"] as? [[String: Any]])?[0] let fields = recordElement?["fields"] as? [String: Any] @@ -591,7 +592,7 @@ final class skyflow_iOS_collectTests: XCTestCase { func testCollectElementSetValueAndClearValue(){ let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInput = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvvElement = container?.create(input: collectInput) @@ -611,8 +612,8 @@ final class skyflow_iOS_collectTests: XCTestCase { func testCollectElementSetValueAndClearValueWithCustomFormatting(){ let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInputfieldInput = Skyflow.CollectElementInput(table: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: Skyflow.ElementType.INPUT_FIELD ) - let requiredOption = Skyflow.CollectElementOptions(required: true, format: "+91 YYYY-YYYY-YYYY YYYY", translation: ["Y": "[0-9]"]) + let collectInputfieldInput = CollectElementInput(tableName: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: ElementType.INPUT_FIELD ) + let requiredOption = CollectElementOptions(required: true, format: "+91 YYYY-YYYY-YYYY YYYY", translation: ["Y": "[0-9]"]) let collectInputField = container?.create(input: collectInputfieldInput, options: requiredOption ) @@ -633,8 +634,8 @@ final class skyflow_iOS_collectTests: XCTestCase { func testCollectElementSetValueAndClearValueWithCustomFormattingWithEmptyTsanslation(){ let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInputfieldInput = Skyflow.CollectElementInput(table: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: Skyflow.ElementType.INPUT_FIELD ) - let requiredOption = Skyflow.CollectElementOptions(required: true, format: "+91 YYYY-YYYY-YYYY YYYY", translation: ["Y": ""]) + let collectInputfieldInput = CollectElementInput(tableName: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: ElementType.INPUT_FIELD ) + let requiredOption = CollectElementOptions(required: true, format: "+91 YYYY-YYYY-YYYY YYYY", translation: ["Y": ""]) let collectInputField = container?.create(input: collectInputfieldInput, options: requiredOption ) @@ -655,8 +656,8 @@ final class skyflow_iOS_collectTests: XCTestCase { func testCollectElementSetValueAndClearValueWithCustomFormattingWithoutTranslation(){ let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInputfieldInput = Skyflow.CollectElementInput(table: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: Skyflow.ElementType.INPUT_FIELD ) - let requiredOption = Skyflow.CollectElementOptions(required: true, format: "+91 YYYY-YYYY-YYYY YYYY") + let collectInputfieldInput = CollectElementInput(tableName: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: ElementType.INPUT_FIELD ) + let requiredOption = CollectElementOptions(required: true, format: "+91 YYYY-YYYY-YYYY YYYY") let collectInputField = container?.create(input: collectInputfieldInput, options: requiredOption ) @@ -677,8 +678,8 @@ final class skyflow_iOS_collectTests: XCTestCase { func testCollectElementSetValueAndClearValueWithoutCustomFormatting(){ let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInputfieldInput = Skyflow.CollectElementInput(table: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: Skyflow.ElementType.INPUT_FIELD ) - let requiredOption = Skyflow.CollectElementOptions(required: true) + let collectInputfieldInput = CollectElementInput(tableName: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: ElementType.INPUT_FIELD ) + let requiredOption = CollectElementOptions(required: true) let collectInputField = container?.create(input: collectInputfieldInput, options: requiredOption ) @@ -699,8 +700,8 @@ final class skyflow_iOS_collectTests: XCTestCase { func testCollectElementSetValueAndClearValueWithoutCustomFormattingForCardNumber(){ let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let collectInputfieldInput = Skyflow.CollectElementInput(table: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: Skyflow.ElementType.CARD_NUMBER ) - let requiredOption = Skyflow.CollectElementOptions(required: true, format: "XXXX-XXXX-XXXX-XXXX") + let collectInputfieldInput = CollectElementInput(tableName: "pii_fields", column: "cardholder_name", label: "input field", placeholder: "card input field", type: ElementType.CARD_NUMBER ) + let requiredOption = CollectElementOptions(required: true, format: "XXXX-XXXX-XXXX-XXXX") let collectInputField = container?.create(input: collectInputfieldInput, options: requiredOption ) @@ -725,251 +726,98 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_ID().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } func testCollectNoVaultURL() { - skyflow.vaultURL = "/v1/vaults/" + skyflow.vaultURL = "/" let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectBadTypeAddionalFields() { - let additionalFields = ["records": "records"] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectNoRecordsInAddionalFields() { - let additionalFields = ["typo": []] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, - ErrorCodes.MISSING_RECORDS_IN_ADDITIONAL_FIELDS() - .getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - func testCollectEmptyRecordsAddionalFields() { - let additionalFields = ["records": []] + let additionalFields = AdditionalFields(records: []) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - func testCollectNoTableKeyAddionalFields() { - let additionalFields = ["records": [[:]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_KEY_ERROR(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectBadTableKeyAddionalFields() { - let additionalFields = ["records": [["table": []]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - + func testCollectEmptyTableAddionalFields() { - let additionalFields = ["records": [["table": ""]]] + let additionalFields = AdditionalFields(records: [AdditionalFieldsRecord(tableName: "", data: ["field": "value"])]) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_TABLE_NAME().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - func testCollectNoFieldsKeyAddionalFields() { - let additionalFields = ["records": [["table": "table"]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.FIELDS_KEY_ERROR(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectInvalidFieldsAddionalFields() { - let additionalFields = ["records": [["table": "table", "fields": "fields"]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_FIELDS_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - + func testCollectEmptyFieldsAddionalFields() { - let additionalFields = ["records": [["table": "table", "fields": [:]]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectEmptyColumnNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1"]] - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - - func testCollectEmptyTableNameForUpsertOption() { + let additionalFields = AdditionalFields(records: [AdditionalFieldsRecord(tableName: "table", data: [:])]) let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["column": "person"]] + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - + func testCollectEmptyUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:[])) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:[])) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectEmptyColumnNameUpsertOption() { + func testCollectEmptyUniqueColumnsUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1", "column": ""]] + let upsertOptions = [UpsertOption(tableName: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } func testCollectEmptyTableNameUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "", "column": "person"]] + let upsertOptions = [UpsertOption(tableName: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - - - func testInsertEmptyColumnNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1"]] - let expectation = XCTestExpectation() - let records = [ - "records" : [[ - "table": "card1", - "fields": [ - "person" : "abcfgdyt", - "cvv" : "567" - ] - ]] - ] - let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) - self.skyflow?.insert(records: records, options: insertOptions, callback: callback) - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) - } - - - func testInsertEmptyTableNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["column": "person"]] - let expectation = XCTestExpectation() - let records = [ - "records" : [[ - "table": "card1", - "fields": [ - "person" : "abcfgdyt", - "cvv" : "567" - ] - ]] - ] - let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) - self.skyflow?.insert(records: records, options: insertOptions, callback: callback) - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) - } - - func testInsertEmptyUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() @@ -983,16 +831,16 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: []) + let insertOptions = InsertOptions(upsert: []) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - func testInsertEmptyColumnNameUpsertOption() { + func testInsertEmptyUniqueColumnsUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1", "column": ""]] + let upsertOptions = [UpsertOption(tableName: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -1004,16 +852,16 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } func testInsertEmptyTableNameUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "", "column": "person"]] + let upsertOptions = [UpsertOption(tableName: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -1025,13 +873,13 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - + func testUnmount() { let container = skyflow.container(type: ContainerType.COLLECT) let date = container?.create(input: CollectElementInput(type: .EXPIRATION_DATE), options: CollectElementOptions(format: "test")) @@ -1106,7 +954,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: Styles(base: Style(cursorColor: .orange)), placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: Styles(base: Style(cursorColor: .orange)), placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) @@ -1119,7 +967,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false, enableCopy: true) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -1142,7 +990,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false, enableCopy: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -1163,7 +1011,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let options = CollectElementOptions(required: false, enableCopy: true) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift new file mode 100644 index 00000000..6c69a2ab --- /dev/null +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the EMPTY_SKYFLOW_ID validation in FlowVaultCollectRequestBody: +// an explicit empty-string skyflowId (on an element or an additionalFields record) +// is rejected client-side instead of silently falling back to an insert. + +import XCTest +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore + +final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration(vaultID: "id", vaultURL: "http://demo.com", tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) + } + + override func tearDown() { + skyflow = nil + } + + private func makeNameElement(skyflowId: String? = nil) -> TextField { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let input = CollectElementInput(tableName: "persons", column: "name", type: .CARDHOLDER_NAME, skyflowId: skyflowId) + let element = container!.create(input: input, options: CollectElementOptions(required: false)) + element.textField.secureText = "John Doe" + // secureText's setter doesn't sync actualValue on its own - that normally happens via + // the textFieldDidChange delegate hook, which a direct assignment here bypasses. + element.updateActualValue() + return element + } + + func testAdditionalFieldsEmptySkyflowIdRejected() { + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId in additionalFields")) + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["name": "demo"], skyflowId: "") + ]) + + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index 0").description) + } + + func testAdditionalFieldsEmptySkyflowIdReportsRecordIndex() { + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Index of offending record")) + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["name": "demo"], skyflowId: "valid-id"), + AdditionalFieldsRecord(tableName: "persons", data: ["age": "30"], skyflowId: "") + ]) + + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index 1").description) + } + + func testElementEmptySkyflowIdRejected() { + let element = makeNameElement(skyflowId: "") + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId on element")) + + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [element], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "element with column 'name'").description) + } + + func testNilSkyflowIdDefaultsToInsert() { + let element = makeNameElement() + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Nil skyflowId inserts")) + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "table1", data: ["email": "a@b.com"]) + ]) + + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [element], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCollectElementInputSkyflowIdDefaultsToNil() { + let input = CollectElementInput(tableName: "persons", column: "name", type: .CARDHOLDER_NAME) + XCTAssertNil(input.data.skyflowId) + } + + func testValidSkyflowIdStillCreatesUpdate() { + let element = makeNameElement(skyflowId: "row-1") + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Valid skyflowId updates")) + + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [element], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["row-1"] as! [String: Any] + XCTAssertEqual(entry["table"] as? String, "persons") + XCTAssertEqual((entry["fields"] as! [String: Any])["name"] as? String, "John Doe") + } +} diff --git a/Tests/skyflow-iOS-composableTests/DemoImpl.swift b/Tests/skyflow-iOS-composableTests/DemoImpl.swift index 281305b8..b8994e2f 100644 --- a/Tests/skyflow-iOS-composableTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-composableTests/DemoImpl.swift @@ -7,7 +7,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index a94cd297..885b137a 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -6,13 +6,14 @@ // import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore final class skyflow_iOS_composableEelementsTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = Skyflow.initialize( + self.skyflow = SkyflowFlowVaultIOS.initialize( Configuration( vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, @@ -33,7 +34,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) _ = container?.create(input: collectInput, options: options) @@ -54,7 +55,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) _ = container?.create(input: collectInput, options: options) @@ -74,7 +75,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) _ = container?.create(input: collectInput, options: options) @@ -94,7 +95,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) _ = container?.create(input: collectInput, options: options) @@ -114,7 +115,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) cardNumber?.actualValue = "4111 1111 1111 1111" @@ -137,25 +138,25 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let collectElement = container?.create(input: collectInput, options: options) - collectElement?.on(eventName: Skyflow.EventName.CHANGE) { state in + collectElement?.on(eventName: EventName.CHANGE) { state in print("state", state) onChangeCalled = true } - collectElement?.on(eventName: Skyflow.EventName.BLUR) { state in + collectElement?.on(eventName: EventName.BLUR) { state in print("state", state) onBlurCalled = true } - collectElement?.on(eventName: Skyflow.EventName.FOCUS) { state in + collectElement?.on(eventName: EventName.FOCUS) { state in print("state", state) onFocusCalled = true } - collectElement?.on(eventName: Skyflow.EventName.READY) { _ in + collectElement?.on(eventName: EventName.READY) { _ in onReadyCalled = true } sleep(1) @@ -177,7 +178,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) do { @@ -213,7 +214,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber", label: "Card Number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber", label: "Card Number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -221,7 +222,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { cardNumber?.textFieldDidEndEditing(cardNumber!.textField) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) @@ -257,7 +258,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: true) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) _ = container?.create(input: collectInput, options: options) do { @@ -283,7 +284,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let mycontainer = skyflow.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [2])) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) let textField = mycontainer?.create(input: collectInput) do { let view = try mycontainer?.getComposableView() @@ -311,7 +312,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let mycontainer = skyflow.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) let textField = mycontainer?.create(input: collectInput) do { let view = try mycontainer?.getComposableView() @@ -338,14 +339,14 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let collectOptions = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "pin", placeholder: "pin", type: .PIN) + let collectInput = CollectElementInput(tableName: "persons", column: "pin", placeholder: "pin", type: .PIN) let pinElement = container?.create(input: collectInput, options: collectOptions) var vs = ValidationSet() vs.add(rule: ElementValueMatchRule(element: pinElement!, error: "ELEMENT NOT MATCHING")) - let collectInput2 = CollectElementInput(table: "persons", column: "", placeholder: "pin", type: .PIN, validations: vs) + let collectInput2 = CollectElementInput(tableName: "persons", column: "", placeholder: "pin", type: .PIN, validations: vs) let confirmPinElement = container?.create(input: collectInput2, options: collectOptions) @@ -371,7 +372,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) cardNumber?.actualValue = "4111 1111 1111 1111" @@ -398,7 +399,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) cardNumber?.actualValue = "4111 1111 1111 1111" @@ -427,10 +428,10 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "cardnumber",inputStyles: styles, label: "Card Number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "cardnumber",inputStyles: styles, label: "Card Number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) @@ -455,7 +456,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { } func testInsertEmptyTableNameForUpsertOption() { _ = skyflow.container(type: ContainerType.COMPOSABLE) - let upsertOptions = [["column": "person"]] + let upsertOptions = [UpsertOption(tableName: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -467,23 +468,24 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - func testCollectBadTableKeyAddionalFields() { - let additionalFields = ["records": [["table": []]]] - let container = skyflow.container(type: ContainerType.COMPOSABLE) - + + func testComposableCollectEmptyVaultURL() { + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let container = clientWithEmptyURL.container(type: ContainerType.COMPOSABLE) + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - + container?.collect(callback: callback) + wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) + + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) } func testCreateRows() { let elements = [1, 2] @@ -587,7 +589,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let options = CollectElementOptions(required: true) - let collectInput = CollectElementInput(table: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput, options: options) do { diff --git a/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift b/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift index cde1fe96..8aa1d148 100644 --- a/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift +++ b/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift @@ -6,43 +6,44 @@ // import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore class CollectElementOptionsTests: XCTestCase { func testCollectElementOptions(){ // default init let options = CollectElementOptions() - XCTAssertEqual(options.format, "mm/yy") - XCTAssertEqual(options.translation, nil) + XCTAssertEqual(options.data.format, "mm/yy") + XCTAssertEqual(options.data.translation, nil) } func testCollectElementOptionsWithFormat(){ // format let options = CollectElementOptions(format: "XXXX") - XCTAssertEqual(options.format, "XXXX") - XCTAssertEqual(options.translation, nil) + XCTAssertEqual(options.data.format, "XXXX") + XCTAssertEqual(options.data.translation, nil) } func testCollectElementOptionsWithTranslation(){ // format let options = CollectElementOptions(translation: ["X": "[0-9]"]) - XCTAssertEqual(options.format, "mm/yy") - XCTAssertEqual(options.translation, ["X": "[0-9]"]) + XCTAssertEqual(options.data.format, "mm/yy") + XCTAssertEqual(options.data.translation, ["X": "[0-9]"]) } func testCollectElementOptionsWithTranslationFormat(){ // format let options = CollectElementOptions(format: "XXX",translation: ["X": "[0-9]"]) - XCTAssertEqual(options.format, "XXX") - XCTAssertEqual(options.translation, ["X": "[0-9]"]) + XCTAssertEqual(options.data.format, "XXX") + XCTAssertEqual(options.data.translation, ["X": "[0-9]"]) } func testCollectElementOptionsWithEmptyFormat(){ // format let options = CollectElementOptions(format: "",translation: ["X": "[0-9]"]) - XCTAssertEqual(options.format, "") - XCTAssertEqual(options.translation, ["X": "[0-9]"]) + XCTAssertEqual(options.data.format, "") + XCTAssertEqual(options.data.translation, ["X": "[0-9]"]) } func testCollectElementOptionsWithNilFormat(){ // format let options = CollectElementOptions(format: "XXX",translation: nil) - XCTAssertEqual(options.format, "XXX") - XCTAssertEqual(options.translation, nil) + XCTAssertEqual(options.data.format, "XXX") + XCTAssertEqual(options.data.translation, nil) } func testCollectElementOptionsWithCardMetaData(){ let options = CollectElementOptions(cardMetaData: ["scheme": [CardType.AMEX, CardType.CARTES_BANCAIRES]]) - XCTAssertEqual(options.cardMetaData as! [String : [CardType]], ["scheme": [CardType.AMEX, CardType.CARTES_BANCAIRES]]) + XCTAssertEqual(options.data.cardMetaData as! [String : [CardType]], ["scheme": [CardType.AMEX, CardType.CARTES_BANCAIRES]]) } diff --git a/Tests/skyflow-iOS-elementTests/DemoImpl.swift b/Tests/skyflow-iOS-elementTests/DemoImpl.swift index 71cb4d32..0f86e36e 100644 --- a/Tests/skyflow-iOS-elementTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-elementTests/DemoImpl.swift @@ -4,7 +4,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { diff --git a/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift b/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift index 0975792f..74e93bc0 100644 --- a/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift +++ b/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift @@ -4,7 +4,8 @@ import XCTest import Foundation -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length class InputFormattingTests: XCTestCase { @@ -12,7 +13,7 @@ class InputFormattingTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = Skyflow.initialize( + self.skyflow = SkyflowFlowVaultIOS.initialize( Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, tokenProvider: DemoTokenProvider()) @@ -27,7 +28,7 @@ class InputFormattingTests: XCTestCase { let options = CollectElementOptions(required: false) - let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let cardNumberInput = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: cardNumberInput, options: options) window.addSubview(cardNumber!) @@ -43,7 +44,7 @@ class InputFormattingTests: XCTestCase { let options = CollectElementOptions(required: false) - let expiryDateInput = CollectElementInput(table: "persons", column: "cvv", placeholder: "expiryDate", type: .EXPIRATION_DATE) + let expiryDateInput = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "expiryDate", type: .EXPIRATION_DATE) let expiryDate = container?.create(input: expiryDateInput, options: options) window.addSubview(expiryDate!) @@ -59,7 +60,7 @@ class InputFormattingTests: XCTestCase { let options = CollectElementOptions(required: false) - let expiryDateInput = CollectElementInput(table: "persons", column: "cvv", placeholder: "expiryDate", type: .EXPIRATION_MONTH) + let expiryDateInput = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "expiryDate", type: .EXPIRATION_MONTH) let expiryDate = container?.create(input: expiryDateInput, options: options) window.addSubview(expiryDate!) @@ -229,8 +230,8 @@ class InputFormattingTests: XCTestCase { let devOptions = ContextOptions(env: .DEV) let cardInput = CollectElementInput(type: .CARD_NUMBER) - let prodField = TextField(input: cardInput, options: CollectElementOptions(), contextOptions: prodOptions) - let devField = TextField(input: cardInput, options: CollectElementOptions(), contextOptions: devOptions, elements: [prodField]) + let prodField = TextField(input: cardInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + let devField = TextField(input: cardInput.data, options: CollectElementOptions().data, contextOptions: devOptions, elements: [prodField]) prodField.actualValue = "4111 1111 1111 1111" devField.actualValue = "4111 1111 1111 1111" @@ -252,8 +253,8 @@ class InputFormattingTests: XCTestCase { let devOptions = ContextOptions(env: .DEV) // cvv - var prodField = TextField(input: cvvInput, options: CollectElementOptions(), contextOptions: prodOptions) - var devField = TextField(input: cvvInput, options: CollectElementOptions(), contextOptions: devOptions) + var prodField = TextField(input: cvvInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + var devField = TextField(input: cvvInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "572" devField.actualValue = "572" @@ -262,8 +263,8 @@ class InputFormattingTests: XCTestCase { XCTAssertEqual((devField.state as! StateforText).getStateForListener()["value"] as? String, "572") // expiryDate - prodField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: prodOptions) - devField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: devOptions) + prodField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + devField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "12/24" devField.actualValue = "12/24" @@ -272,8 +273,8 @@ class InputFormattingTests: XCTestCase { XCTAssertEqual((devField.state as! StateforText).getStateForListener()["value"] as? String, "12/24") // expiryYear - prodField = TextField(input: expirationYearInput, options: CollectElementOptions(), contextOptions: prodOptions) - devField = TextField(input: expirationYearInput, options: CollectElementOptions(), contextOptions: devOptions) + prodField = TextField(input: expirationYearInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + devField = TextField(input: expirationYearInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "2024" devField.actualValue = "2024" @@ -282,8 +283,8 @@ class InputFormattingTests: XCTestCase { XCTAssertEqual((devField.state as! StateforText).getStateForListener()["value"] as? String, "2024") // expiryMonth - prodField = TextField(input: expirationMonthInput, options: CollectElementOptions(), contextOptions: prodOptions) - devField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: devOptions) + prodField = TextField(input: expirationMonthInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + devField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "12" devField.actualValue = "12" @@ -292,8 +293,8 @@ class InputFormattingTests: XCTestCase { XCTAssertEqual((devField.state as! StateforText).getStateForListener()["value"] as? String, "12") // cardholderName - prodField = TextField(input: nameInput, options: CollectElementOptions(), contextOptions: prodOptions) - devField = TextField(input: nameInput, options: CollectElementOptions(), contextOptions: devOptions) + prodField = TextField(input: nameInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + devField = TextField(input: nameInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "John" devField.actualValue = "John" @@ -302,8 +303,8 @@ class InputFormattingTests: XCTestCase { XCTAssertEqual((devField.state as! StateforText).getStateForListener()["value"] as? String, "John") // PIN - prodField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: prodOptions) - devField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: devOptions) + prodField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + devField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "1234" devField.actualValue = "1234" @@ -312,8 +313,8 @@ class InputFormattingTests: XCTestCase { XCTAssertEqual((devField.state as! StateforText).getStateForListener()["value"] as? String, "1234") // generic - prodField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: prodOptions) - devField = TextField(input: expirationDateInput, options: CollectElementOptions(), contextOptions: devOptions) + prodField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: prodOptions) + devField = TextField(input: expirationDateInput.data, options: CollectElementOptions().data, contextOptions: devOptions) prodField.actualValue = "1234" devField.actualValue = "1234" @@ -329,8 +330,8 @@ class InputFormattingTests: XCTestCase { let prodOptions = ContextOptions() let devOptions = ContextOptions(env: .DEV) - let prodField = TextField(input: amexInput, options: CollectElementOptions(), contextOptions: prodOptions, elements: []) - let devField = TextField(input: amexInput, options: CollectElementOptions(), contextOptions: devOptions, elements: []) + let prodField = TextField(input: amexInput.data, options: CollectElementOptions().data, contextOptions: prodOptions, elements: []) + let devField = TextField(input: amexInput.data, options: CollectElementOptions().data, contextOptions: devOptions, elements: []) prodField.actualValue = "378282246310005" devField.actualValue = "378282246310005" diff --git a/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift b/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift index 03bb05c1..b1caad42 100644 --- a/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift +++ b/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift @@ -7,7 +7,8 @@ import XCTest import Foundation -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -15,7 +16,7 @@ class TextFieldDelegateTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = Skyflow.initialize( + self.skyflow = SkyflowFlowVaultIOS.initialize( Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, tokenProvider: DemoTokenProvider()) @@ -30,7 +31,7 @@ class TextFieldDelegateTests: XCTestCase { let collectOptions = CollectElementOptions(required: false, format: "+91 YYYY-YYYY-YYYY YYYY XXXX", translation: ["X": "[A-Z]", "Y": "[0-9]", "Z": "[A-Za-z0-9]"]) - let inputFieldInput = CollectElementInput(table: "persons", column: "input_field", placeholder: "input_field", type: .INPUT_FIELD) + let inputFieldInput = CollectElementInput(tableName: "persons", column: "input_field", placeholder: "input_field", type: .INPUT_FIELD) let inputField = container?.create(input: inputFieldInput, options: collectOptions) window.addSubview(inputField!) @@ -45,7 +46,7 @@ class TextFieldDelegateTests: XCTestCase { let collectOptions = CollectElementOptions(required: false, format: "+91 YYYY-YYYY-YYYY YYYY XXXX") - let inputFieldInput = CollectElementInput(table: "persons", column: "input_field", placeholder: "input_field", type: .INPUT_FIELD) + let inputFieldInput = CollectElementInput(tableName: "persons", column: "input_field", placeholder: "input_field", type: .INPUT_FIELD) let inputField = container?.create(input: inputFieldInput, options: collectOptions) window.addSubview(inputField!) @@ -60,7 +61,7 @@ class TextFieldDelegateTests: XCTestCase { let collectOptions = CollectElementOptions(required: false, format: "+91 XX XXXX XXXX", translation: ["X": "[0-9]"]) - let inputFieldInput = CollectElementInput(table: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) + let inputFieldInput = CollectElementInput(tableName: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) let inputField = container?.create(input: inputFieldInput, options: collectOptions) window.addSubview(inputField!) @@ -75,7 +76,7 @@ class TextFieldDelegateTests: XCTestCase { let collectOptions = CollectElementOptions(required: false, format: "(XXX) XXX-XXXX", translation: ["X": "[0-9]"]) - let inputFieldInput = CollectElementInput(table: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) + let inputFieldInput = CollectElementInput(tableName: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) let inputField = container?.create(input: inputFieldInput, options: collectOptions) window.addSubview(inputField!) @@ -90,7 +91,7 @@ class TextFieldDelegateTests: XCTestCase { let collectOptions = CollectElementOptions(required: false, format: "XXX-XX-XXXX", translation: ["X": "[0-9]"]) - let inputFieldInput = CollectElementInput(table: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) + let inputFieldInput = CollectElementInput(tableName: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) let inputField = container?.create(input: inputFieldInput, options: collectOptions) window.addSubview(inputField!) @@ -105,7 +106,7 @@ class TextFieldDelegateTests: XCTestCase { let collectOptions = CollectElementOptions(required: false, format: "X-YYYYYYY", translation: ["X": "[A-Z]", "Y": "[0-9]"]) - let inputFieldInput = CollectElementInput(table: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) + let inputFieldInput = CollectElementInput(tableName: "persons", column: "input_field", placeholder: "phone Number", type: .INPUT_FIELD) let inputField = container?.create(input: inputFieldInput, options: collectOptions) window.addSubview(inputField!) diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift b/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift index 8a68e011..d8983c01 100644 --- a/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift +++ b/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift @@ -4,7 +4,8 @@ import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length class skyflow_iOS_containerTests: XCTestCase { diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift index b6e84385..4e9b0c26 100644 --- a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift +++ b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift @@ -5,7 +5,8 @@ import XCTest import UIKit -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length class skyflow_iOS_elementTests: XCTestCase { @@ -20,14 +21,14 @@ class skyflow_iOS_elementTests: XCTestCase { self.collectOptions = CollectElementOptions(required: false) - self.collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) + self.collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) - skyflowElement = SkyflowElement(input: collectInput!, options: collectOptions!, contextOptions: ContextOptions(), elements: [] ) + skyflowElement = SkyflowElement(input: collectInput!.data, options: collectOptions!.data, contextOptions: ContextOptions(), elements: [] ) - textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) - let revealElementInput = RevealElementInput(token: "token", label: "RevealElement", redaction: .DEFAULT) - label = Label(input: revealElementInput, options: RevealElementOptions()) + let revealElementInput = RevealElementInput(token: "token", label: "RevealElement") + label = Label(input: revealElementInput.data, options: RevealElementOptions().data) } override func tearDown() { @@ -131,8 +132,8 @@ class skyflow_iOS_elementTests: XCTestCase { let myRules = ValidationSet(rules: [myRegexRule]) - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "invalid" textField.textFieldDidEndEditing(textField.textField) @@ -146,8 +147,8 @@ class skyflow_iOS_elementTests: XCTestCase { let myRules = ValidationSet(rules: [myRandomRule, myRegexRule]) - let collectInput = CollectElementInput(table: "tablename", column: "column", placeholder: "John Doe", type: .INPUT_FIELD, validations: myRules) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "tablename", column: "column", placeholder: "John Doe", type: .INPUT_FIELD, validations: myRules) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) // Default UI error @@ -169,8 +170,8 @@ class skyflow_iOS_elementTests: XCTestCase { let myRules = ValidationSet(rules: [myRegexRule]) - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER, validations: myRules) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" textField.textFieldDidEndEditing(textField.textField) @@ -180,8 +181,8 @@ class skyflow_iOS_elementTests: XCTestCase { func testTriggerError() { let errorStyle = Style(textColor: .red) let inputStyle = Styles(invalid: errorStyle) - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", inputStyles: inputStyle, placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", inputStyles: inputStyle, placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "invalid" textField.setError("triggered error") @@ -195,8 +196,8 @@ class skyflow_iOS_elementTests: XCTestCase { func testResetError() { let errorStyle = Style(textColor: .red) let inputStyle = Styles(invalid: errorStyle) - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", inputStyles: inputStyle, placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", inputStyles: inputStyle, placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "invalid" textField.setError("triggered error") @@ -318,21 +319,21 @@ class skyflow_iOS_elementTests: XCTestCase { } func getElementForDropDownTesting()-> TextField { - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" return textField } func getElementForDropDownTestingRightIcon()-> TextField { - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .right)),placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: CollectElementOptions(enableCardIcon: true, enableCopy: true), contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .right)),placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: CollectElementOptions(enableCardIcon: true, enableCopy: true).data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" return textField } func getElementOfOtherType()-> TextField { - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .right)),placeholder: "card number", type: .CVV) - let textField = TextField(input: collectInput, options: CollectElementOptions(enableCopy: true), contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .right)),placeholder: "card number", type: .CVV) + let textField = TextField(input: collectInput.data, options: CollectElementOptions(enableCopy: true).data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "1234" return textField } @@ -442,8 +443,8 @@ class skyflow_iOS_elementTests: XCTestCase { } func getElementForDropDownTestingWindow() -> TextField { - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: collectOptions.data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" return textField } @@ -471,15 +472,15 @@ class skyflow_iOS_elementTests: XCTestCase { } func testDisableCardIconWhenEnableCopyTrue(){ - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: CollectElementOptions(enableCardIcon: false, enableCopy: true), contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: CollectElementOptions(enableCardIcon: false, enableCopy: true).data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" textField.textFieldDidEndEditing(textField.textField) XCTAssertTrue(textField.cardIconContainerView.isHidden) } func testDisableCardIconWhenEnableCopyFalse(){ - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: CollectElementOptions(enableCardIcon: false, enableCopy: false), contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: CollectElementOptions(enableCardIcon: false, enableCopy: false).data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" textField.textFieldDidEndEditing(textField.textField) XCTAssertTrue(textField.cardIconContainerView.isHidden) @@ -552,8 +553,8 @@ class skyflow_iOS_elementTests: XCTestCase { } } func testGetDropDownIconSetsUpButtonLeftView() { - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .left)),placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: CollectElementOptions(enableCardIcon: true, enableCopy: true), contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .left)),placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: CollectElementOptions(enableCardIcon: true, enableCopy: true).data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111" textField.textFieldDidEndEditing(textField.textField) @@ -571,8 +572,8 @@ class skyflow_iOS_elementTests: XCTestCase { } } func testGetDropDownIconSetsUpButtonRightViewEnableCopyFalse() { - let collectInput = CollectElementInput(table: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .right)),placeholder: "card number", type: .CARD_NUMBER) - let textField = TextField(input: collectInput, options: CollectElementOptions(enableCardIcon: true, enableCopy: false), contextOptions: ContextOptions(), elements: []) + let collectInput = CollectElementInput(tableName: "persons", column: "cardNumber", iconStyles: Styles(base: Style(cardIconAlignment: .right)),placeholder: "card number", type: .CARD_NUMBER) + let textField = TextField(input: collectInput.data, options: CollectElementOptions(enableCardIcon: true, enableCopy: false).data, contextOptions: ContextOptions(), elements: []) textField.textField.secureText = "4111111111111111"; textField.textFieldDidEndEditing(textField.textField) textField.update(updateOptions: CollectElementOptions(cardMetaData: ["scheme": [CardType.AMEX]])) if #available(iOS 14.0, *) { diff --git a/Tests/skyflow-iOS-errorTests/DemoImpl.swift b/Tests/skyflow-iOS-errorTests/DemoImpl.swift index 71cb4d32..a9ac3d2a 100644 --- a/Tests/skyflow-iOS-errorTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-errorTests/DemoImpl.swift @@ -4,7 +4,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { @@ -41,13 +42,19 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: CollectResponse? + var revealResponse: RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { - if responseBody is String { + if let response = responseBody as? CollectResponse { + self.collectResponse = response + } else if let response = responseBody as? RevealResponse { + self.revealResponse = response + } else if responseBody is String { self.receivedResponse = responseBody as! String } else { @@ -66,4 +73,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: CollectCallback { + CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: RevealCallback { + RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-errorTests/GatewayImpl.swift b/Tests/skyflow-iOS-errorTests/GatewayImpl.swift index c163759e..9439d630 100644 --- a/Tests/skyflow-iOS-errorTests/GatewayImpl.swift +++ b/Tests/skyflow-iOS-errorTests/GatewayImpl.swift @@ -4,7 +4,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class ConnectionTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 3df67f16..4d80b75d 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -12,7 +12,8 @@ import XCTest import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class Skyflow_iOS_collectErrorTests: XCTestCase { @@ -60,7 +61,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -74,7 +75,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -92,7 +93,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -112,7 +113,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -131,7 +132,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -151,7 +152,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -173,7 +174,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) @@ -184,7 +185,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -200,7 +201,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -208,7 +209,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) @@ -219,7 +220,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -233,7 +234,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) @@ -244,7 +245,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -256,18 +257,18 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "card_number", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) cvv?.textField.secureText = "211" window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - Duplicate Elements") let callback = DemoAPICallback(expectation: expectation) - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -278,65 +279,51 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) cvv?.textField.secureText = "211" window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "cvv": "123", - "name": "John Doe" - ]] - ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["cvv": "123", "name": "John Doe"]) + ]) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: ["cvv", "persons"]).description) } - + func testCreateRequestBodyDuplicateInAdditionalFields() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) cvv?.textField.secureText = "211" window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "duplicate": "123", - "name": "John Doe" - ]], - [ - "table": "persons", - "fields": [ - "duplicate": "123", - ]] - ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["duplicate": "123", "name": "John Doe"]), + AdditionalFieldsRecord(tableName: "persons", data: ["duplicate": "123"]) + ]) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: "duplicate").description) } @@ -344,157 +331,352 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) cvv?.textField.secureText = "211" window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "cvv": "123", - ]], - [ - "table": "persons", - "fields": [ - "duplicate": "123", - ]] - ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["cvv": "123"]), + AdditionalFieldsRecord(tableName: "persons", data: ["duplicate": "123"]) + ]) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: ["cvv", "persons"]).description) } - + func testCreateRequestBodyWithOnlyInserts() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) + let collectInput2 = CollectElementInput(tableName: "persons", column: "cvv", placeholder: "cvv", type: .CVV) let cvv = container?.create(input: collectInput2, options: options) cvv?.textField.secureText = "211" window.addSubview(cvv!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertNotNil(requestBody?["records"]) XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ ["table": "persons", "fields": ["card_number": "", "cvv": ""]] ]) - XCTAssertEqual(requestBody?["update"] as? [String: String], [:]) + // Every plain CollectElementInput defaults to skyflowId: "" (not nil) - confirms that + // default doesn't accidentally get treated as "update targeting an empty ID". + XCTAssertTrue((requestBody?["update"] as! [String: Any]).isEmpty) } - func testCreateRequestBodyWithOnlyUpdates() { - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"], - ["table": "table1", "fields": ["column2": "value2"], "skyflowID": "id1"] - ] - ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + func testCreateRequestBodyExplicitEmptyStringSkyflowIdOnElementIsRejected() { + // An explicit empty-string skyflowId is rejected client-side (see + // skyflow_iOS_emptySkyflowIdValidationTests) rather than silently falling back to insert. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + let collectInput = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowId: "") + let cardNumber = container?.create(input: collectInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Explicit empty-string skyflowId is rejected")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "element with column 'card_number'").description) + } + + func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "table1", data: ["column1": "value1"], skyflowId: "id1") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "table1") + XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) + } + + func testCreateRequestBodyEmptyStringSkyflowIdOnAdditionalFieldsIsRejected() { + // An explicit empty-string skyflowId is rejected client-side (see + // skyflow_iOS_emptySkyflowIdValidationTests) rather than silently falling back to insert. + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "table1", data: ["column1": "value1"], skyflowId: "") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId is rejected")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index 0").description) + } + + func testCreateRequestBodyMergesMultipleAdditionalFieldsSharingSkyflowId() { + // Two additionalFields entries that both target the same skyflowId should merge their + // fields into a single update payload entry, not clobber or duplicate it. + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["name": "John"], skyflowId: "id1"), + AdditionalFieldsRecord(tableName: "persons", data: ["email": "john@example.com"], skyflowId: "id1") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Merge additionalFields sharing a skyflowId")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["update"]) - XCTAssertEqual(requestBody?["update"] as? NSDictionary, ["id1": ["table": "table1", "fields": ["column1": "value1", "column2": "value2"], "skyflowID": "id1"]]) - XCTAssertEqual(requestBody?["records"] as? [[String: String]], []) + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + let fields = entry["fields"] as! [String: String] + XCTAssertEqual(fields["name"], "John") + XCTAssertEqual(fields["email"], "john@example.com") } - func testCreateRequestBodyWithOnlyUpdates2() { + func testCreateRequestBodyWithSkyflowIDOnElement() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(tableName: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowId: "id1") let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["records"]) - XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ - ["table": "persons", "fields": ["card_number": "", "cvv": ""]] + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) + } + + func testCreateRequestBodyMixedInsertAndUpdateElements() { + // One plain element (no skyflowId) and one update-by-skyflowId element in the same + // collect() call - the plain one should land in "records", the tagged one in "update", + // without either bucket interfering with the other. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(tableName: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let updateInput = CollectElementInput(tableName: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed insert and update elements")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "cards") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let updateEntry = update["id1"] as! [String: Any] + XCTAssertEqual(updateEntry["table"] as! String, "persons") + } + + func testCreateRequestBodyPlainInsertViaAdditionalFieldsCombinedWithElement() { + // additionalFields without a skyflowId is a plain insert, same as a mounted element - + // both should end up in "records" together, none in "update". + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(tableName: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "accounts", data: ["status": "active"]) + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element insert + additionalFields insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.contains { $0["table"] as? String == "cards" }) + XCTAssertTrue(records.contains { $0["table"] as? String == "accounts" }) + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyElementUpdateWithAdditionalFieldsInsert() { + // An update-by-skyflowId element combined with a plain-insert additionalFields record - + // "records" should only contain the additionalFields insert, "update" only the element. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let updateInput = CollectElementInput(tableName: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "accounts", data: ["status": "active"]) ]) - XCTAssertEqual(requestBody?["update"] as? [String: String], [:]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element update + additionalFields insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "accounts") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + XCTAssertEqual((update["id1"] as! [String: Any])["table"] as! String, "persons") } - func testCreateRequestBodyWithOnlyUpdates3() { + func testFullCombinationCollectUpdateUpsertAndAdditionalFields() { + // The kitchen-sink scenario: an insert element, an update-by-skyflowId element, an + // insert additionalFields record, and an update additionalFields record, all in one + // request - then upsert applied on top. Upsert only ever touches FlowVaultInsertRequestBody's + // "records" bucket (confirmed: FlowVaultUpdateRequestBody never reads options.upsert at + // all), so it should only decorate the matching insert record and leave "update" untouched. let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowID: "id1") - let cardNumber = container?.create(input: collectInput1, options: options) - cardNumber?.textField.secureText = "4111 1111 1111 1111" - window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV, skyflowID: "id2") - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"], - ["table": "table1", "fields": ["column2": "value2"], "skyflowID": "id1"] - ] - ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + let insertInput = CollectElementInput(tableName: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let updateInput = CollectElementInput(tableName: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "accounts", data: ["status": "active"]), + AdditionalFieldsRecord(tableName: "billing", data: ["zip": "94105"], skyflowId: "id2") + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Full combination")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["update"]) - XCTAssertEqual(requestBody?["update"] as? NSDictionary, ["id1": ["table": "table1", "fields": ["column1": "value1", "column2": "value2", "card_number": ""], "skyflowID": "id1"], "id2": ["table": "persons", "fields": ["cvv": ""], "skyflowID": "id2"]]) - XCTAssertEqual(requestBody?["records"] as? [[String: String]], []) + + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.contains { $0["table"] as? String == "cards" }) + XCTAssertTrue(records.contains { $0["table"] as? String == "accounts" }) + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 2) + XCTAssertEqual((update["id1"] as! [String: Any])["table"] as! String, "persons") + XCTAssertEqual((update["id2"] as! [String: Any])["table"] as! String, "billing") + + // Apply upsert on top, matching only the "cards" table. + let upsertOptions = [UpsertOption(tableName: "cards", uniqueColumns: ["card_number"], updateType: .UPDATE)] + let wireBody = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: requestBody!, options: FlowVaultICOptions(upsert: upsertOptions)) + let wireRecords = wireBody["records"] as! [[String: Any]] + + let cardsWireRecord = wireRecords.first { $0["tableName"] as? String == "cards" } + XCTAssertNotNil(cardsWireRecord?["upsert"]) + + let accountsWireRecord = wireRecords.first { $0["tableName"] as? String == "accounts" } + XCTAssertNil(accountsWireRecord?["upsert"]) + + // update bucket is a completely separate path (FlowVaultUpdateRequestBody) - upsert has + // no way to reach or affect it, confirmed structurally since FlowVaultInsertRequestBody + // only ever reads requestBody["records"]. + XCTAssertEqual(update.count, 2) } - func testCreateRequestBodyWithMixedInsertsAndUpdates() { + // Not currently reachable from any documented flow (README only shows ElementValueMatchRule + // used to allow a duplicate plain/insert element, not two update-by-skyflowId elements), but + // the code path exists in FlowVaultCollectRequestBody's update branch too - covering it now + // in case a future update UI (e.g. confirm-password-style re-entry on an editable field) relies on it. + func testCreateRequestBodyElementValueMatchRuleBypassesUpdateDuplicate() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) - let cardNumber = container?.create(input: collectInput1, options: options) - cardNumber?.textField.secureText = "4111 1111 1111 1111" - window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV, skyflowID: "id2") - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column3": "value3"], "skyflowID": "id1"] + + let updateInput1 = CollectElementInput(tableName: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement1 = container?.create(input: updateInput1, options: options) + updateElement1?.textField.secureText = "John" + updateElement1?.textFieldDidEndEditing(updateElement1!.textField) + window.addSubview(updateElement1!) + + var vs = ValidationSet() + vs.add(rule: ElementValueMatchRule(element: updateElement1!, error: "ELEMENT NOT MATCHING")) + let updateInput2 = CollectElementInput(tableName: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, validations: vs, skyflowId: "id1") + let updateElement2 = container?.create(input: updateInput2, options: options) + updateElement2?.textField.secureText = "Jane" + updateElement2?.textFieldDidEndEditing(updateElement2!.textField) + window.addSubview(updateElement2!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should not fail")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement1!, updateElement2!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let entry = update["id1"] as! [String: Any] + // Second element's value is skipped (continue), not merged over the first's. + XCTAssertEqual((entry["fields"] as! [String: String])["name"], "John") + } + + func testInsertEmptyVaultURL() { + let expectation = XCTestExpectation(description: "Insert with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.insert(records: ["records": [["table": "table", "fields": ["field": "value"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .INSERT)).localizedDescription) + } + + func testCollectCallbackOnFailureParsesStructuredAPIError() { + let wholeRequestFailure: [String: Any] = [ + "error": [ + "grpcCode": 13, + "httpCode": 500, + "message": "Skyflow services experienced an internal error. Contact Skyflow support with request ID 2db2c594-b9a5-48db-a220-f936e12a43e9 for more information.", + "httpStatus": "Internal Server Error", + "details": [] ] ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed inserts and updates")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) - XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["records"]) - let records = requestBody?["records"] as? [[String: Any]] - XCTAssertEqual(records?.count, 1) - XCTAssertEqual(records?[0] as! NSDictionary as NSDictionary, ["table": "persons", "fields": ["card_number": ""]]) - XCTAssertNotNil(requestBody?["update"]) - let update = requestBody?["update"] as? [String: Any] - let updateRecords = update?["id1"] as! [String: Any] - XCTAssertEqual(updateRecords as NSDictionary, ["table": "table1", "fields": ["column3": "value3"], "skyflowID": "id1"]) + var receivedError: SkyflowError? + let collectCallback = CollectCallback( + onSuccess: { _ in XCTFail("onSuccess should not be called") }, + onFailure: { error in receivedError = error } + ) + + collectCallback.onFailure(wholeRequestFailure) + + XCTAssertEqual(receivedError?.httpCode, 500) + XCTAssertEqual(receivedError?.message, "Skyflow services experienced an internal error. Contact Skyflow support with request ID 2db2c594-b9a5-48db-a220-f936e12a43e9 for more information.") + XCTAssertEqual(receivedError?.grpcCode, 13) + XCTAssertEqual(receivedError?.httpStatus, "Internal Server Error") + XCTAssertEqual(receivedError?.details?.count, 0) } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift index c5dfc98d..cca5f177 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift @@ -10,7 +10,8 @@ // import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length class Skyflow_iOS_generalErrorTests: XCTestCase { @@ -69,14 +70,6 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { } - func testGetByIDRecord() { - let record = GetByIdRecord(ids: ["id1", "id2"], table: "table", redaction: "DEFAULT") - - XCTAssertEqual(record.ids, ["id1", "id2"]) - XCTAssertEqual("table", record.table) - XCTAssertEqual(record.redaction, "DEFAULT") - } - func testValidationSet() { let validationSet = ValidationSet( rules: [SkyflowValidateCardNumber( @@ -104,7 +97,7 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { XCTAssert(textRect.contains(CGPoint(x: 1, y: 2))) XCTAssert(placeholderRect.contains(CGPoint(x: 4, y: 3))) XCTAssert(editingRect.contains(CGPoint(x: 2, y: 4))) - XCTAssertEqual(textfield.description, "Skyflow.FormatTextField") + XCTAssertEqual(textfield.description, NSStringFromClass(FormatTextField.self)) } func testState() { @@ -146,8 +139,63 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { func testIsTokenValid() { let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) let expectation = XCTestExpectation(description: "should get token") - + XCTAssertEqual(false, apiClient.isTokenValid()) } + + func testSkyflowErrorWrapReturnsExistingInstanceUnchanged() { + let original = SkyflowError(domain: "TestDomain", code: 123, userInfo: [NSLocalizedDescriptionKey: "original message"]) + let wrapped = SkyflowError.wrap(original) + XCTAssertTrue(wrapped === original) + } + + func testSkyflowErrorWrapPreservesPlainNSError() { + let nsError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + let wrapped = SkyflowError.wrap(nsError) + + XCTAssertEqual(wrapped.domain, "NSURLErrorDomain") + XCTAssertEqual(wrapped.httpCode, -1009) + XCTAssertEqual(wrapped.message, "The Internet connection appears to be offline.") + XCTAssertNil(wrapped.grpcCode) + XCTAssertNil(wrapped.httpStatus) + XCTAssertNil(wrapped.details) + } + + func testSkyflowErrorWrapFallsBackForOpaqueValue() { + // Anything that isn't a SkyflowError, the structured {"error": {...}} API shape, an + // NSError, or the internal {"errors": [...]} wrapping still needs to produce *something* + // usable rather than crash. + let wrapped = SkyflowError.wrap("just a plain string, not an error at all") + XCTAssertEqual(wrapped.httpCode, 0) + XCTAssertTrue(wrapped.message.contains("just a plain string")) + } + + func testSkyflowAPIErrorInitMissingOptionalFields() { + let apiError: [String: Any] = [ + "error": ["httpCode": 500, "message": "Internal error"] + ] + let skyflowError = SkyflowError(apiError: apiError) + + XCTAssertEqual(skyflowError?.httpCode, 500) + XCTAssertEqual(skyflowError?.message, "Internal error") + XCTAssertNil(skyflowError?.grpcCode) + XCTAssertNil(skyflowError?.httpStatus) + XCTAssertNil(skyflowError?.details) + } + + func testSkyflowAPIErrorInitReturnsNilForMalformedInput() { + XCTAssertNil(SkyflowError(apiError: "not a dictionary")) + XCTAssertNil(SkyflowError(apiError: ["message": "no nested error key"])) + } + + func testSkyflowErrorWrapRecoversNSErrorFromNestedErrorsArray() { + // Mirrors what FlowVaultRevealAPICallback.callRevealOnFailure produces for a + // whole-request failure reaching Client.detokenize() directly (no RevealValueCallback + // in between to unwrap it first). + let underlying = NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid bearer token"]) + let wrapped = SkyflowError.wrap(["errors": [["error": underlying]]]) + XCTAssertEqual(wrapped.httpCode, 400) + XCTAssertEqual(wrapped.message, "Invalid bearer token") + } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index 038716e6..c0cef4c1 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -11,7 +11,8 @@ import Foundation import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length class Skyflow_iOS_revealErrorTests: XCTestCase { @@ -48,26 +49,8 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { } } - func getByIDFromClientWithExpectation(description: String = "should get records", records: [String: Any]) -> String { - let expectRecords = XCTestExpectation(description: description) - let callback = DemoAPICallback(expectation: expectRecords) - skyflow.getById(records: records, callback: callback) - - wait(for: [expectRecords], timeout: 10.0) - if callback.receivedResponse.isEmpty { - if callback.data["errors"] != nil { - return (callback.data["errors"] as! [NSError])[0].localizedDescription - } else { - return "ok" - } - } else { - return callback.receivedResponse - } - } - - func testDetokenizeNoRecords() { - let records = ["typo": [["token": revealTestId, "redaction": RedactionType.DEFAULT]]] + let records = ["typo": [["token": revealTestId]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.RECORDS_KEY_ERROR().description) } @@ -77,7 +60,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.INVALID_RECORDS_TYPE().description) } - + func testDetokenizeEmptyRecords() { let records = ["records": []] let result = getDataFromClientWithExpectation(records: records) @@ -85,21 +68,16 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { } func testDetokenizeNoTokens() { - let records = ["records": [["redaction": RedactionType.DEFAULT]]] + let records = ["records": [["foo": "bar"]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.ID_KEY_ERROR().description) } func testDetokenizeBadTokens() { - let records = ["records": [["token": [], "redaction": RedactionType.DEFAULT]]] + let records = ["records": [["token": []]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_TYPE(value: "0").description) } - func testDetokenizeInvalidRedaction() { - let records = ["records": [["token": "123233232", "redaction": "123"]]] - let result = getDataFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_REDACTION_TYPE().description) - } func testContainerRevealWithUnmountedElements() { @@ -112,26 +90,12 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should return reveal output")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) let result = callback.receivedResponse XCTAssertEqual(result, ErrorCodes.UNMOUNTED_REVEAL_ELEMENT(value: revealTestId).description) } - func testContainerRevealWithNoRedaction() { // when redaction is not specify plaintext will be applied - let window = UIWindow() - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - - let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) - let styles = Styles(base: bstyle) - - let revealElementInput = RevealElementInput(token: "232h3j23h2jh",inputStyles: styles, label: "RevealElement") - let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - - window.addSubview(revealElement!) - XCTAssertEqual(revealElement!.revealInput.redaction, .PLAIN_TEXT) - } - func testContainerRevealWithEmptyToken() { let window = UIWindow() let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) @@ -139,64 +103,44 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) let styles = Styles(base: bstyle) - let revealElementInput = RevealElementInput(inputStyles: styles, label: "RevealElement", redaction: .DEFAULT) + let revealElementInput = RevealElementInput(inputStyles: styles, label: "RevealElement") let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) window.addSubview(revealElement!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should return reveal output")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) let result = callback.receivedResponse XCTAssertEqual(result, ErrorCodes.EMPTY_TOKEN_ID().description) } - func testGetByIdNoRecords() { - let records = ["ok": [["redaction": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.EMPTY_RECORDS_OBJECT().description) - } + func testContainerRevealEmptyVaultURL() { + // Unlike Client.detokenize()/getById()/get(), RevealContainer.reveal() calls + // callback.onFailure directly with the raw NSError (no callRevealOnFailure wrapping). + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let revealContainer = clientWithEmptyURL.container(type: ContainerType.REVEAL, options: nil) - func testGetByIdInvalidRecords() { - let records = ["records": 12] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_RECORDS_TYPE().description) - } + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Reveal with empty vaultURL should fail")) + revealContainer?.reveal(callback: callback.asRevealCallback) - func testGetByIdNoIds() { - let records = ["records": [["redaction": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.MISSING_KEY_IDS(value: "0").description) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .REVEAL_CONTAINER)).localizedDescription) } - func testGetByIdInvalidIds() { - let records = ["records": [["ids": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_IDS_TYPE().description) - } + func testDetokenizeEmptyVaultURL() { + // Client.detokenize()'s vault-level errors route through callRevealOnFailure, which + // wraps the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "Detokenize with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) - func testGetByIdNoTable() { - let records = ["records": [["ids": ["abc"]]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.TABLE_KEY_ERROR(value: "0").description) - } - - func testGetByIdInvalidTable() { - let records = ["records": [["ids": ["abc"], "table": ["abc"]]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").description) - } + clientWithEmptyURL.detokenize(records: ["records": [["token": "sometoken"]]], callback: callback) - func testGetByIdNoRedaction() { - let records = ["records": [["ids": ["abc"], "table": "table"]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.REDACTION_KEY_ERROR(value: "0").description) + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) } - func testGetByIdInvalidRedaction() { - let records = ["records": [["ids": ["abc"], "table": "table", "redaction": "DEFAULT"]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_REDACTION_TYPE().description) - } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift new file mode 100644 index 00000000..7133fcb0 --- /dev/null +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the tokenGroupRedactions validation in RevealContainer.reveal +// (port of the JS SDK's validateRevealOptions): every entry must have a non-empty, +// non-whitespace tokenGroupName and redaction; failures report the entry index. + +import Foundation +import XCTest +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore + +class Skyflow_iOS_revealOptionsValidationTests: XCTestCase { + var skyflow: Client! + var window: UIWindow! + + override func setUp() { + self.skyflow = Client(Configuration(vaultID: "id", vaultURL: "http://demo.com", tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) + self.window = UIWindow() + } + + override func tearDown() { + skyflow = nil + window = nil + } + + // Builds a container with one mounted, token-bearing element so the reveal call + // gets past the unmounted/empty-token checks and reaches the options validation. + private func makeContainerWithMountedElement() -> Container? { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let input = RevealElementInput(token: "some-token", inputStyles: Styles(), label: "RevealElement") + let element = container?.create(input: input, options: RevealElementOptions()) + window.addSubview(element!) + return container + } + + private func revealAndCaptureError(options: RevealOptions) -> String { + let container = makeContainerWithMountedElement() + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should fail validation")) + container?.reveal(callback: callback.asRevealCallback, options: options) + return callback.receivedResponse + } + + func testEmptyTokenGroupNameRejected() { + let result = revealAndCaptureError(options: RevealOptions(tokenGroupRedactions: [ + TokenGroupRedaction(tokenGroupName: "", redaction: "plain_text") + ])) + XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: "0").description) + } + + func testEmptyRedactionRejected() { + let result = revealAndCaptureError(options: RevealOptions(tokenGroupRedactions: [ + TokenGroupRedaction(tokenGroupName: "deterministic", redaction: "") + ])) + XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: "0").description) + } + + func testWhitespaceOnlyTokenGroupNameRejected() { + let result = revealAndCaptureError(options: RevealOptions(tokenGroupRedactions: [ + TokenGroupRedaction(tokenGroupName: " ", redaction: "plain_text") + ])) + XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: "0").description) + } + + func testWhitespaceOnlyRedactionRejected() { + let result = revealAndCaptureError(options: RevealOptions(tokenGroupRedactions: [ + TokenGroupRedaction(tokenGroupName: "deterministic", redaction: "\n\t ") + ])) + XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: "0").description) + } + + func testInvalidEntryReportsItsIndex() { + // First entry valid, second invalid: index 1 in the error proves the valid + // entry passed and the offending one is identified precisely. + let result = revealAndCaptureError(options: RevealOptions(tokenGroupRedactions: [ + TokenGroupRedaction(tokenGroupName: "deterministic", redaction: "plain_text"), + TokenGroupRedaction(tokenGroupName: "nondeterministic", redaction: " ") + ])) + XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: "1").description) + } +} diff --git a/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift b/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift index 4b656b88..698dc696 100644 --- a/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift @@ -5,6 +5,7 @@ import Foundation import XCTest import Skyflow +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { diff --git a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift index 315d8832..276ec1a9 100644 --- a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift +++ b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift @@ -12,6 +12,7 @@ import Foundation import XCTest @testable import Skyflow +@testable import SkyflowCore // swiftlint:disable:next type_body_length class skyflow_iOS_getByIdTests: XCTestCase { @@ -49,14 +50,14 @@ class skyflow_iOS_getByIdTests: XCTestCase { ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID3"]! ], "table": "persons", - "redaction": Skyflow.RedactionType.PLAIN_TEXT + "redaction": RedactionType.PLAIN_TEXT ], [ "ids": [ ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID3"]! ], "table": "persons", - "redaction": Skyflow.RedactionType.PLAIN_TEXT + "redaction": RedactionType.PLAIN_TEXT ] ] ] @@ -74,5 +75,20 @@ class skyflow_iOS_getByIdTests: XCTestCase { XCTAssertEqual(errorMessage, "TokenProvider error") } - + + func testGetByIdEmptyVaultURL() { + // Client.getById()'s vault-level errors route through callRevealOnFailure, which + // wraps the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "getById with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.getById(records: ["records": [["ids": ["id1"], "table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_getByIdUtilTests.swift b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift similarity index 99% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_getByIdUtilTests.swift rename to Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift index 77c97572..26933df9 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_getByIdUtilTests.swift +++ b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift @@ -5,6 +5,7 @@ // swiftlint:disable file_length import XCTest @testable import Skyflow +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class skyflow_iOS_getByIdUtilTests: XCTestCase { diff --git a/Tests/skyflow-iOS-getTests/DemoImpl.swift b/Tests/skyflow-iOS-getTests/DemoImpl.swift index 47e94797..997f488f 100644 --- a/Tests/skyflow-iOS-getTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-getTests/DemoImpl.swift @@ -5,6 +5,7 @@ import Foundation import XCTest import Skyflow +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift b/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift index eabf5a0b..53cf5ccd 100644 --- a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift +++ b/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift @@ -8,6 +8,7 @@ import Foundation import XCTest @testable import Skyflow +@testable import SkyflowCore class skyflow_iOS_getTests: XCTestCase { var skyflow: Client! @@ -44,14 +45,14 @@ class skyflow_iOS_getTests: XCTestCase { "3" ], "table": "persons", - "redaction": Skyflow.RedactionType.PLAIN_TEXT + "redaction": RedactionType.PLAIN_TEXT ], [ "ids": [ "1" ], "table": "persons", - "redaction": Skyflow.RedactionType.PLAIN_TEXT + "redaction": RedactionType.PLAIN_TEXT ] ] ] @@ -88,14 +89,14 @@ class skyflow_iOS_getTests: XCTestCase { "3" ], "table": "persons", - "redaction": Skyflow.RedactionType.PLAIN_TEXT + "redaction": RedactionType.PLAIN_TEXT ], [ "ids": [ "1" ], "table": "persons", - "redaction": Skyflow.RedactionType.PLAIN_TEXT + "redaction": RedactionType.PLAIN_TEXT ] ] ] @@ -108,6 +109,20 @@ class skyflow_iOS_getTests: XCTestCase { print(callback.receivedResponse) XCTAssertTrue(callback.data.description.contains(ErrorCodes.REDACTION_WITH_TOKEN_NOT_SUPPORTED().description)) } - - + + func testGetEmptyVaultURL() { + // Client.get()'s vault-level errors route through callRevealOnFailure, which wraps + // the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "get with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.get(records: ["records": [["ids": ["id1"], "table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_getUtilTests.swift b/Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift similarity index 99% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_getUtilTests.swift rename to Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift index bffc6d45..6c73edc3 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_getUtilTests.swift +++ b/Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import Skyflow +@testable import SkyflowCore final class skyflow_iOS_getUtilTests: XCTestCase { diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift b/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift index 29d9157a..1f887851 100644 --- a/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift +++ b/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift @@ -8,6 +8,7 @@ import Foundation import XCTest @testable import Skyflow +@testable import SkyflowCore class skyflow_iOS_ValidateGetRecordTest: XCTestCase { var skyflow: Client! diff --git a/Tests/skyflow-iOS-legacyTests/DemoImpl.swift b/Tests/skyflow-iOS-legacyTests/DemoImpl.swift new file mode 100644 index 00000000..28c195b4 --- /dev/null +++ b/Tests/skyflow-iOS-legacyTests/DemoImpl.swift @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +import Foundation +import XCTest +import Skyflow +import SkyflowCore + +public class DemoTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: Callback) { + apiCallback.onSuccess("dummy_token") + } +} + +public class DemoAPICallback: Callback { + var receivedResponse: String = "" + var expectation: XCTestExpectation + var data: [String: Any] = [:] + + public init(expectation: XCTestExpectation) { + self.expectation = expectation + } + + public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? String { + self.receivedResponse = response + } else { + self.data = responseBody as! [String: Any] + } + expectation.fulfill() + } + + public func onFailure(_ error: Any) { + if error is NSError { + self.receivedResponse = String((error as! Error).localizedDescription) + } else if error is [String: Any] { + self.data = (error as! [String: Any]) + } + expectation.fulfill() + } +} diff --git a/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift b/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift new file mode 100644 index 00000000..64a125f9 --- /dev/null +++ b/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Minimal URLProtocol-based network mock. Register via URLProtocol.registerClass(MockURLProtocol.self) +// before the request fires (URLSession(configuration: .default) consults globally registered +// protocol classes), and unregister afterward. Used for tests that need to exercise real +// URLSession.dataTask call sites (e.g. FlowVaultInsertAPICallback's insert+update merge), where +// bypassing the network layer entirely (as most other tests do via processResponse(data:response:error:)) +// wouldn't exercise the merge logic itself. + +import Foundation + +final class MockURLProtocol: URLProtocol { + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + client?.urlProtocol(self, didFailWithError: NSError(domain: "MockURLProtocol", code: -1, userInfo: [NSLocalizedDescriptionKey: "No requestHandler configured"])) + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift new file mode 100644 index 00000000..b554cac0 --- /dev/null +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB CollectAPICallback/InsertAPICallback (kept for potential +// future PDB reuse). + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_dormantCollectApiCallbackTests: XCTestCase { + var collectCallback: CollectAPICallback! + var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] + + override func setUp() { + self.collectCallback = CollectAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: defaultRecord, + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + } + + func testBuildFieldsDict() { + let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] + let result = self.collectCallback.buildFieldsDict(dict: dict) + XCTAssertEqual(dict["key"] as! String, result["key"] as! String) + XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) + } + + func testOnSuccessWithNoInsertAndNoUpdateRecords() { + // No network call should be made at all when there's nothing to insert or update. + let expectation = XCTestExpectation(description: "Empty batch should succeed with an empty response") + let callback = DemoAPICallback(expectation: expectation) + let emptyCallback = CollectAPICallback( + callback: callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": []], + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + + emptyCallback.onSuccess("token") + wait(for: [expectation], timeout: 10.0) + + XCTAssertTrue(callback.data.isEmpty) + } + + func testGetRequestSession() { + let url = URL(string: "https://example.org")! + do { + let (request, session) = try self.collectCallback.getRequestSession(url: url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] + let records = body["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table") + XCTAssertNotNil(session) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkError() { + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + do { + let result = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "offline") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testProcessResponseBadStatusCode() { + let responseDict = ["error": ["message": "Internal Server Error"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + let result = try self.collectCallback.processResponse(data: data, response: httpResponse, error: nil) + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "Internal Server Error - request-id: RID") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGetCollectResponseBody() { + // Old v1 gateway-batch response shape: {"responses": [{"records": [{"skyflow_id": ...}]}]} + // getCollectResponseBody walks self.records["records"] (the INPUT records), reading the + // matching entry out of the responses array at the same index - so the input record count + // must match, not the responses array's own length. + let twoRecordCallback = CollectAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": [["table": "table1", "fields": ["field": "value1"]], ["table": "table2", "fields": ["field": "value2"]]]], + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + let responseDict: [String: Any] = [ + "responses": [ + ["records": [["skyflow_id": "SID1"]]], + ["records": [["skyflow_id": "SID2"]]] + ] + ] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let result = try twoRecordCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["table"] as? String, "table1") + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + XCTAssertEqual(records[1]["table"] as? String, "table2") + XCTAssertEqual(records[1]["skyflow_id"] as? String, "SID2") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessUpdateResponseSuccess() { + let responseDict: [String: Any] = ["skyflow_id": "SID1"] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let result = try self.collectCallback.processUpdateResponse(data: data, response: httpResponse, error: nil, table: "table") + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + XCTAssertEqual(records[0]["table"] as? String, "table") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessUpdateResponseBadStatusCode() { + let responseDict = ["error": ["message": "Record not found"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + let result = try self.collectCallback.processUpdateResponse(data: data, response: httpResponse, error: nil, table: "table") + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "Record not found") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testOnFailure() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + self.collectCallback.callback = callback + + self.collectCallback.onFailure(["errors": [["error": "boom"]]]) + wait(for: [expectation], timeout: 10.0) + + XCTAssertNotNil(callback.data["errors"]) + } +} + +final class skyflow_iOS_dormantInsertApiCallbackTests: XCTestCase { + var insertCallback: InsertAPICallback! + var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] + + override func setUp() { + self.insertCallback = InsertAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: defaultRecord, + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + } + + func testBuildFieldsDict() { + let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] + let result = self.insertCallback.buildFieldsDict(dict: dict) + XCTAssertEqual(dict["key"] as! String, result["key"] as! String) + XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) + } + + func testGetRequestSession() { + let url = URL(string: "https://example.org")! + do { + let (request, session) = try self.insertCallback.getRequestSession(url: url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] + let records = body["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table") + XCTAssertNotNil(session) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkErrorThrows() { + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + do { + _ = try self.insertCallback.processResponse(data: nil, response: nil, error: networkError) + XCTFail("Should throw on a connection-level error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + + func testProcessResponseBadStatusCodeThrows() { + let responseDict = ["error": ["message": "Internal Server Error"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + _ = try self.insertCallback.processResponse(data: data, response: httpResponse, error: nil) + XCTFail("Should throw on a bad status code") + } catch { + XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") + } + } + + func testGetCollectResponseBody() { + let responseDict: [String: Any] = [ + "responses": [ + ["records": [["skyflow_id": "SID1"]]] + ] + ] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let result = try self.insertCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as? String, "table") + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testOnFailure() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + self.insertCallback.callback = callback + + self.insertCallback.onFailure(["errors": [["error": "boom"]]]) + wait(for: [expectation], timeout: 10.0) + + XCTAssertNotNil(callback.data["errors"]) + } +} diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift new file mode 100644 index 00000000..12959649 --- /dev/null +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB CollectRequestBody (kept for potential future PDB reuse). +// Distinct from FlowVaultCollectRequestBody, which is the live class covering this same logic. + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_dormantCollectRequestBodyTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration(vaultID: "id", vaultURL: "http://demo.com", tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) + } + + func testCreateRequestBodySimpleInsert() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER) + let cardNumber = container?.create(input: cardNumberInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Pure insert")) + let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as? String, "persons") + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { + let additionalFields: [String: Any] = [ + "records": [ + ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"] + ] + ] + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) + let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "table1") + XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) + } + + func testCreateRequestBodyWithSkyflowIDOnElement() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER, skyflowID: "id1") + let cardNumber = container?.create(input: cardNumberInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) + let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) + } + + func testCreateRequestBodyDuplicateAdditionalFieldError() { + let additionalFields: [String: Any] = [ + "records": [ + ["table": "table1", "fields": ["column1": "value1"]], + ["table": "table1", "fields": ["column1": "value2"]] + ] + ] + let expectation = XCTestExpectation(description: "Duplicate additional field should fail") + let callback = DemoAPICallback(expectation: expectation) + let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: "column1").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testGetUniqueColumn() { + let upsert: [[String: Any]] = [ + ["table": "table1", "column": "email"], + ["table": "table2", "column": "ssn"] + ] + + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table1", upsert: upsert), "email") + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table2", upsert: upsert), "ssn") + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table3", upsert: upsert), "") + } +} diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift new file mode 100644 index 00000000..f8b38e3c --- /dev/null +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Dormant v1 PDB helper tests (APIClient batch/upsert helpers and ICOptions). + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_legacyUtilTests: XCTestCase { + // Dormant v1 PDB helpers on APIClient (untyped upsert), kept for potential future PDB reuse. + + func testGetUniqueColumn() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let upsert: [[String: Any]] = [ + ["table": "table1", "column": "email"], + ["table": "table2", "column": "ssn"] + ] + + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table1", upsert: upsert), "email") + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table2", upsert: upsert), "ssn") + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table3", upsert: upsert), "") + } + + func testConstructBatchRequestBodyWithUpsert() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + XCTAssertEqual(postPayload.count, 1) + XCTAssertEqual(postPayload[0]["upsert"] as? String, "email") + XCTAssertEqual(postPayload[0]["tableName"] as? String, "table1") + XCTAssertEqual(postPayload[0]["method"] as? String, "POST") + XCTAssertEqual(postPayload[0]["quorum"] as? Bool, true) + } + + func testConstructBatchRequestBodyWithTokensAddsGetStep() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let options = ICOptions(tokens: true) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + // One POST insert entry, plus one GET tokenization entry appended after it. + XCTAssertEqual(postPayload.count, 2) + XCTAssertEqual(postPayload[0]["method"] as? String, "POST") + XCTAssertEqual(postPayload[1]["method"] as? String, "GET") + XCTAssertEqual(postPayload[1]["tokenization"] as? Bool, true) + XCTAssertEqual(postPayload[1]["ID"] as? String, "$responses.0.records.0.skyflow_id") + } + + func testConstructBatchRequestBodyNoUpsertNoTokens() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let options = ICOptions(tokens: false) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + XCTAssertEqual(postPayload.count, 1) + XCTAssertNil(postPayload[0]["upsert"]) + } + + // Dormant v1 ICOptions.validateUpsert() (untyped upsert dicts), kept for potential future + // PDB reuse. Distinct from FlowVaultICOptions.validateUpsert(), which covers the live path. + + func testICOptionsValidateUpsertValid() { + let callback = DemoAPICallback(expectation: XCTestExpectation()) + let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertFalse(options.validateUpsert()) + } + + func testICOptionsValidateUpsertEmptyArray() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let options = ICOptions(tokens: false, upsert: [], callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertMissingTableKey() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertMissingColumnKey() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "table1"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertEmptyTableValue() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertEmptyColumnValue() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "table1", "column": ""]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } +} diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift new file mode 100644 index 00000000..4ca47a28 --- /dev/null +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB RevealAPICallback (kept for potential future PDB reuse). + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_revealApiCallbackTests: XCTestCase { + var revealApiCallback: RevealAPICallback! + var expectation: XCTestExpectation! + var callback: DemoAPICallback! + + override func setUp() { + self.expectation = XCTestExpectation() + self.callback = DemoAPICallback(expectation: self.expectation) + self.revealApiCallback = RevealAPICallback( + callback: self.callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + connectionUrl: "https://example.org/v1/vaults/vault", + records: [RevealRequestRecord(token: "token1", redaction: "PLAIN_TEXT")], + contextOptions: ContextOptions() + ) + } + + func testGetRequestSession() { + let (request, _) = self.revealApiCallback.getRequestSession() + + XCTAssertEqual(request.url?.absoluteString, "https://example.org/v1/vaults/vault/detokenize") + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json; utf-8") + XCTAssertEqual(request.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + } + + func testGetRevealRequestBody() { + do { + let record = RevealRequestRecord(token: "abc123", redaction: "PLAIN_TEXT") + let data = try self.revealApiCallback.getRevealRequestBody(record: record) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + let params = json["detokenizationParameters"] as! [[String: Any]] + + XCTAssertEqual(params.count, 1) + XCTAssertEqual(params[0]["token"] as? String, "abc123") + XCTAssertEqual(params[0]["redaction"] as? String, "PLAIN_TEXT") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseSuccess() { + do { + let record = RevealRequestRecord(token: "abc123", redaction: "PLAIN_TEXT") + let responseDict = ["records": [["token": "abc123", "value": "revealed-value"]]] + let data = try JSONSerialization.data(withJSONObject: responseDict) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + + let (success, failure) = try self.revealApiCallback.processResponse(record: record, data: data, response: httpResponse, error: nil) + + XCTAssertNil(failure) + XCTAssertEqual(success?.token_id, "abc123") + XCTAssertEqual(success?.value, "revealed-value") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseBadStatusCode() { + do { + let record = RevealRequestRecord(token: "badtoken", redaction: "PLAIN_TEXT") + let responseDict = ["error": ["message": "Invalid Token"]] + let data = try JSONSerialization.data(withJSONObject: responseDict) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + + let (success, failure) = try self.revealApiCallback.processResponse(record: record, data: data, response: httpResponse, error: nil) + + XCTAssertNil(success) + XCTAssertEqual(failure?.id, "badtoken") + XCTAssertEqual(failure?.error.localizedDescription, "Invalid Token - request-id: RID") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkError() { + let record = RevealRequestRecord(token: "abc123", redaction: "PLAIN_TEXT") + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + + do { + _ = try self.revealApiCallback.processResponse(record: record, data: nil, response: nil, error: networkError) + XCTFail("Should throw on a connection-level error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + + func testHandleCallbacksAllSuccess() { + let success = [RevealSuccessRecord(token_id: "t1", value: "v1")] + + self.revealApiCallback.handleCallbacks(success: success, failure: [], isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "t1") + XCTAssertEqual(records[0]["value"] as? String, "v1") + XCTAssertNil(self.callback.data["errors"]) + } + + func testHandleCallbacksPartialFailure() { + let success = [RevealSuccessRecord(token_id: "t1", value: "v1")] + let failure = [RevealErrorRecord(id: "t2", error: NSError(domain: "", code: 404, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]))] + + self.revealApiCallback.handleCallbacks(success: success, failure: failure, isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + let records = self.callback.data["records"] as! [[String: Any]] + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["token"] as? String, "t2") + } + + func testHandleCallbacksAllFailure() { + let failure = [RevealErrorRecord(id: "t1", error: NSError(domain: "", code: 404, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]))] + + self.revealApiCallback.handleCallbacks(success: [], failure: failure, isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + XCTAssertNil(self.callback.data["records"]) + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 1) + } + + func testHandleCallbacksTransportFailure() { + // isSuccess = false represents a request-level (network) failure somewhere in the batch, + // as opposed to a per-token error - it should route through callRevealOnFailure instead. + let networkError = NSError(domain: "", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + + self.revealApiCallback.handleCallbacks(success: [], failure: [], isSuccess: false, errorObject: networkError) + wait(for: [self.expectation], timeout: 10.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "offline") + } + + func testOnSuccessInvalidUrl() { + self.revealApiCallback.connectionUrl = "invalid url" + self.revealApiCallback.onSuccess("token") + wait(for: [self.expectation], timeout: 20.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "unsupported URL") + } + + func testOnFailureWrapsSwiftError() { + let error = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "boom"]) + + self.revealApiCallback.onFailure(error) + wait(for: [self.expectation], timeout: 10.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "boom") + } + + func testOnFailurePassesThroughNonErrorValues() { + let dict: [String: Any] = ["errors": [["error": "already a dict"]]] + + self.revealApiCallback.onFailure(dict) + wait(for: [self.expectation], timeout: 10.0) + + XCTAssertNotNil(self.callback.data["errors"]) + } +} diff --git a/Tests/skyflow-iOS-revealTests/DemoImpl.swift b/Tests/skyflow-iOS-revealTests/DemoImpl.swift index 73208cc0..7c40c321 100644 --- a/Tests/skyflow-iOS-revealTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-revealTests/DemoImpl.swift @@ -4,7 +4,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { @@ -35,12 +36,24 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: CollectResponse? + var revealResponse: RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? CollectResponse { + self.collectResponse = response + expectation.fulfill() + return + } + if let response = responseBody as? RevealResponse { + self.revealResponse = response + expectation.fulfill() + return + } defer{ expectation.fulfill() } @@ -64,4 +77,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: CollectCallback { + CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: RevealCallback { + RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift b/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift index 32d6688b..25c8ff14 100644 --- a/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift +++ b/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift @@ -8,41 +8,42 @@ import Foundation import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore class RevealElementOptionsTests: XCTestCase { func testRevealElementOptionsTestWithoutFormatTranslation(){ let revealElementsOptions = RevealElementOptions() - XCTAssertEqual(revealElementsOptions.format, nil) - XCTAssertEqual(revealElementsOptions.translation, nil) + XCTAssertEqual(revealElementsOptions.data.format, nil) + XCTAssertEqual(revealElementsOptions.data.translation, nil) } func testRevealElementOptionsTestWithFormat(){ let revealElementsOptions = RevealElementOptions(format: "XXXX-XXXX-XXXX") - XCTAssertEqual(revealElementsOptions.format, "XXXX-XXXX-XXXX") - XCTAssertEqual(revealElementsOptions.translation, nil) + XCTAssertEqual(revealElementsOptions.data.format, "XXXX-XXXX-XXXX") + XCTAssertEqual(revealElementsOptions.data.translation, nil) } func testRevealElementOptionsTestWithTranslation(){ let revealElementsOptions = RevealElementOptions(translation: ["X": "[0-9]"]) - XCTAssertEqual(revealElementsOptions.format, nil) - XCTAssertEqual(revealElementsOptions.translation, ["X": "[0-9]"]) + XCTAssertEqual(revealElementsOptions.data.format, nil) + XCTAssertEqual(revealElementsOptions.data.translation, ["X": "[0-9]"]) } func testRevealElementOptionsTestWithBoth(){ let revealElementsOptions = RevealElementOptions(format: "XXXX-XXXX-XXXX", translation: ["X": "[0-9]"]) - XCTAssertEqual(revealElementsOptions.format, "XXXX-XXXX-XXXX") - XCTAssertEqual(revealElementsOptions.translation, ["X": "[0-9]"]) + XCTAssertEqual(revealElementsOptions.data.format, "XXXX-XXXX-XXXX") + XCTAssertEqual(revealElementsOptions.data.translation, ["X": "[0-9]"]) } func testRevealElementOptionsTestWithEnableTrue(){ let revealElementsOptions = RevealElementOptions(format: "XXXX-XXXX-XXXX", translation: ["X": "[0-9]"], enableCopy: true) - XCTAssertEqual(revealElementsOptions.format, "XXXX-XXXX-XXXX") - XCTAssertEqual(revealElementsOptions.translation, ["X": "[0-9]"]) - XCTAssertEqual(revealElementsOptions.enableCopy, true) + XCTAssertEqual(revealElementsOptions.data.format, "XXXX-XXXX-XXXX") + XCTAssertEqual(revealElementsOptions.data.translation, ["X": "[0-9]"]) + XCTAssertEqual(revealElementsOptions.data.enableCopy, true) } func testRevealElementOptionsTestWithFalseEnable(){ let revealElementsOptions = RevealElementOptions(format: "XXXX-XXXX-XXXX", translation: ["X": "[0-9]"]) - XCTAssertEqual(revealElementsOptions.format, "XXXX-XXXX-XXXX") - XCTAssertEqual(revealElementsOptions.translation, ["X": "[0-9]"]) - XCTAssertEqual(revealElementsOptions.enableCopy, false) + XCTAssertEqual(revealElementsOptions.data.format, "XXXX-XXXX-XXXX") + XCTAssertEqual(revealElementsOptions.data.translation, ["X": "[0-9]"]) + XCTAssertEqual(revealElementsOptions.data.enableCopy, false) } } diff --git a/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift b/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift index 5bc478bb..5e1680c8 100644 --- a/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift +++ b/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift @@ -8,32 +8,33 @@ import Foundation import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore class RevealSkyflowLabelViewTests: XCTestCase { func testSkyflowLabelViewUpdateValMethod(){ - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) label.updateVal(value: "123") XCTAssertEqual(label.getValue(), "123") XCTAssertEqual(label.actualValue, "123") } func testSkyflowLabelViewUpdateValMethodWithEmptyFormatTranslation(){ - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions(format: "")) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions(format: "").data) label.updateVal(value: "123") XCTAssertEqual(label.actualValue, "123") } func testSkyflowLabelViewUpdateValMethodWithFormatEmptyTranslation(){ - let config = Skyflow.Configuration(vaultID: "vault id", vaultURL:"vault url", tokenProvider: DemoTokenProvider(), options: Skyflow.Options(env: Skyflow.Env.DEV)) + let config = Configuration(vaultID: "vault id", vaultURL:"vault url", tokenProvider: DemoTokenProvider(), options: Options(env: Env.DEV)) // Initialize skyflow client - let skyflowClient = Skyflow.initialize(config) + let skyflowClient = SkyflowFlowVaultIOS.initialize(config) // Create a Reveal Container - let container = skyflowClient.container(type: Skyflow.ContainerType.REVEAL) + let container = skyflowClient.container(type: ContainerType.REVEAL) // Create Reveal Elements - let cardNumberInput = Skyflow.RevealElementInput( + let cardNumberInput = RevealElementInput( token: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", label: "cardnumber", altText: "XXXX XXXX XXXX XXXX" @@ -46,65 +47,65 @@ class RevealSkyflowLabelViewTests: XCTestCase { XCTAssertEqual(cardNumberElement?.getValue(), "12345") } func testSkyflowLabelViewFormatInputMethodWithEmptyFormatTranslation(){ - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "1234", format: "X-X-X-X", translation: ["X": "[0-9]"]) XCTAssertEqual(result, "1-2-3-4") } func testSkyflowLabelViewFormatInputMethodCase2(){ - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "", format: "X-X-X-X", translation: ["X": "[0-9]"]) XCTAssertEqual(result, "") } func testSkyflowLabelViewFormatInputMethodCase3(){ // when reveal data is less than format length - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "123", format: "X-X-X-X", translation: ["X": "[0-9]"]) XCTAssertEqual(result, "1-2-3") } func testSkyflowLabelViewFormatInputMethodCase4(){ // when reveal data is greater than format length - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "12345", format: "X-X-X-X", translation: ["X": "[0-9]"]) XCTAssertEqual(result, "1-2-3-4") } func testSkyflowLabelViewFormatInputMethodCase5(){ // when translation doesnt contain format character - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "12345", format: "X-X-X-X", translation: ["Y": "[0-9]"]) XCTAssertEqual(result, "X-X-X-X") } func testSkyflowLabelViewFormatInputMethodCase6(){ // when translation doesnt contain format character - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "12345", format: "", translation: ["X": "[0-9]"]) XCTAssertEqual(result, "") } func testSkyflowLabelViewFormatInputMethodCase7(){ // when reveal data is not same as regex in translation - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "12345", format: "XXXX", translation: ["X": "[A-Z]"]) XCTAssertEqual(result, "") } func testSkyflowLabelViewFormatInputMethodCase8(){ // when reveal data is not same as regex in translation - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "ZA", format: "XXXX", translation: ["X": "[ZA]"]) XCTAssertEqual(result, "ZA") } func testSkyflowLabelViewFormatInputMethodCase9(){ // when reveal data type is not same as regex in translation - let label = Label(input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions()) + let label = Label(input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions().data) let result = label.formatInput(input: "ZA", format: "XXXX", translation: ["X": "[-]"]) XCTAssertEqual(result, "") } func testCopyEnabled() { - let label = SkyflowLabelView( input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions(enableCopy: true)) + let label = SkyflowLabelView( input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions(enableCopy: true).data) label.updateVal(value: "demo", actualValue: "demo") XCTAssertTrue(label.label.copyAfterReveal) } func testCopyDisabled() { - let label = SkyflowLabelView( input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions(enableCopy: false)) + let label = SkyflowLabelView( input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions(enableCopy: false).data) label.updateVal(value: "demo", actualValue: nil) XCTAssertTrue(label.label.copyAfterReveal == false) } // func testCopyIconTapped() { -// let label = SkyflowLabelView( input: RevealElementInput(token: "token", label: "label"), options: RevealElementOptions(enableCopy: true)) +// let label = SkyflowLabelView( input: RevealElementInput(token: "token", label: "label").data, options: RevealElementOptions(enableCopy: true).data) // label.label.text = "Hello, World!" // label.updateVal(value: "demo", actualValue: "value") // XCTAssertTrue(label.label.copyAfterReveal) diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index 101264b8..f50668ee 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -5,7 +5,8 @@ import Foundation import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length class skyflow_iOS_revealTests: XCTestCase { @@ -38,15 +39,6 @@ class skyflow_iOS_revealTests: XCTestCase { let istyle = Style(textColor: .red) let styles = Styles(base: bstyle, invalid: istyle) - let revealElementInput = RevealElementInput(token: revealTestId, inputStyles: styles, label: "RevealElement", redaction: .DEFAULT) - - return revealElementInput - } - func getRevealElementInputNoRedaction() -> RevealElementInput { - let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) - let istyle = Style(textColor: .red) - let styles = Styles(base: bstyle, invalid: istyle) - let revealElementInput = RevealElementInput(token: revealTestId, inputStyles: styles, label: "RevealElement") return revealElementInput @@ -64,9 +56,22 @@ class skyflow_iOS_revealTests: XCTestCase { func testRevealElementInput() { let revealElementInput = getRevealElementInput() - XCTAssertEqual(revealElementInput.token, revealTestId) - XCTAssertEqual(revealElementInput.redaction, .DEFAULT) - XCTAssertEqual(revealElementInput.label, "RevealElement") + XCTAssertEqual(revealElementInput.data.token, revealTestId) + XCTAssertEqual(revealElementInput.data.label, "RevealElement") + } + + func testRevealOptionsCarriesTokenGroupRedactions() { + let options = RevealOptions(tokenGroupRedactions: [TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED")]) + + XCTAssertEqual(options.tokenGroupRedactions?.count, 1) + XCTAssertEqual(options.tokenGroupRedactions?.first?.tokenGroupName, "deterministic_string") + XCTAssertEqual(options.tokenGroupRedactions?.first?.redaction, "MASKED") + } + + func testRevealOptionsDefaultsToNilTokenGroupRedactions() { + let options = RevealOptions() + + XCTAssertNil(options.tokenGroupRedactions) } func testCreateSkyflowRevealContainer() { @@ -82,7 +87,7 @@ class skyflow_iOS_revealTests: XCTestCase { XCTAssertEqual(labelView!.padding, UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5)) XCTAssertEqual(labelView!.textColor, .blue) XCTAssertEqual(labelView!.label.secureText, revealTestId) - XCTAssertEqual(labelField.text, revealElementInput.label) + XCTAssertEqual(labelField.text, revealElementInput.data.label) } func testCheckRevealElementsArray() { @@ -104,22 +109,11 @@ class skyflow_iOS_revealTests: XCTestCase { let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) let revealElementInput = getRevealElementInput() let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - - let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] - - let result: [String: [[String: Any]]] = ["records": [["token": revealTestId, "redaction": RedactionType.DEFAULT]]] - - XCTAssertTrue(compareDictionaries(dict1: result, dict2: requestBody)) - } - func testCreateRevealRequestBodyWithNoRedaction() { - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - let revealElementInput = getRevealElementInputNoRedaction() - let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - + let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] - - let result: [String: [[String: Any]]] = ["records": [["token": revealTestId, "redaction": RedactionType.PLAIN_TEXT]]] - + + let result: [String: [[String: Any]]] = ["records": [["token": revealTestId]]] + XCTAssertTrue(compareDictionaries(dict1: result, dict2: requestBody)) } @@ -170,7 +164,7 @@ class skyflow_iOS_revealTests: XCTestCase { func testSetErrorOnReveal() { let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) var revealElementInput = getRevealElementInput() - revealElementInput.token = "invalidtoken" + revealElementInput.data.token = "invalidtoken" let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) let errorMessage = "Triggered Error" @@ -182,7 +176,7 @@ class skyflow_iOS_revealTests: XCTestCase { window.addSubview(revealElement!) let callback = DemoAPICallback(expectation: expectFailure) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) wait(for: [expectFailure], timeout: 10.0) diff --git a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift b/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift index a0189fdb..a3329686 100644 --- a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift +++ b/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift @@ -4,6 +4,7 @@ import Foundation @testable import Skyflow +@testable import SkyflowCore enum MethodsUnderTest { case PUREINSERT @@ -24,13 +25,13 @@ class ClientScenario { } func setVaultUrl(vaultURL: String) -> ClientScenario{ - self.config.vaultURL = vaultURL + self.config.data.vaultURL = vaultURL return self } func setVaultID(vaultId: String) -> ClientScenario{ - self.config.vaultID = vaultId + self.config.data.vaultID = vaultId return self } @@ -47,7 +48,7 @@ class ClientScenario { func getById(ids: [String: Any], callback: Callback) { Client(self.config).getById(records: ids, callback: callback) } - + private func detokenizeRequestBody(_ tokens: [String]) -> [String: [[String: String]]]{ var records = [] as [[String: String]] diff --git a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift b/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift index 29e985e0..c8509fc9 100644 --- a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift @@ -5,6 +5,7 @@ import Foundation import XCTest import Skyflow +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { @@ -33,7 +34,6 @@ public class DemoTokenProvider: TokenProvider { public class DemoAPICallback: Callback { var receivedResponse: String = "" - var xml: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] var error: NSError? = nil @@ -54,9 +54,6 @@ public class DemoAPICallback: Callback { } else if error is [String: Any] { self.data = (error as! [String: Any]) } - if error is SkyflowError { - self.xml = (error as! SkyflowError).getXML() - } expectation.fulfill() } diff --git a/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift b/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift index 27fab3d2..885b7022 100644 --- a/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift +++ b/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift @@ -4,6 +4,7 @@ import Foundation @testable import Skyflow +@testable import SkyflowCore class DetokenizeScenario { private var tokens: [String] = [] diff --git a/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift b/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift index aec14d48..aee0b8a4 100644 --- a/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift +++ b/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift @@ -4,6 +4,7 @@ import Foundation @testable import Skyflow +@testable import SkyflowCore class GetByIdScenario { private var records: [String: [[String: Any]]] = [:] diff --git a/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift b/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift index 1c77d40f..148a6e18 100644 --- a/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift +++ b/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift @@ -5,6 +5,7 @@ import Foundation @testable import Skyflow +@testable import SkyflowCore class InsertScenario { private var records: [String: [[String: Any]]] = [:] diff --git a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift b/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift index 5534cdf6..e2277ac5 100644 --- a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift +++ b/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift @@ -5,6 +5,7 @@ // swiftlint:disable file_length import XCTest @testable import Skyflow +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class skyflow_iOS_detokenizeScenarioTests: XCTestCase { diff --git a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift b/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift index 2b5e9a7b..856f7431 100644 --- a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift +++ b/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift @@ -5,6 +5,7 @@ // swiftlint:disable file_length import XCTest @testable import Skyflow +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class skyflow_iOS_getByIdScenarioTests: XCTestCase { diff --git a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift b/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift index 5c9cf739..fe95350d 100644 --- a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift +++ b/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift @@ -5,6 +5,7 @@ // swiftlint:disable file_length import XCTest @testable import Skyflow +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class skyflow_iOS_insertScenarioTests: XCTestCase { diff --git a/Tests/skyflow-iOS-utilTests/DemoImpl.swift b/Tests/skyflow-iOS-utilTests/DemoImpl.swift index e93c1169..8de90a93 100644 --- a/Tests/skyflow-iOS-utilTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-utilTests/DemoImpl.swift @@ -4,7 +4,8 @@ import Foundation import XCTest -import Skyflow +import SkyflowFlowVaultIOS +import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { @@ -16,6 +17,8 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: CollectResponse? + var revealResponse: RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation @@ -23,7 +26,11 @@ public class DemoAPICallback: Callback { public func onSuccess(_ responseBody: Any) { print("success") - if let response = responseBody as? String { + if let response = responseBody as? CollectResponse { + self.collectResponse = response + } else if let response = responseBody as? RevealResponse { + self.revealResponse = response + } else if let response = responseBody as? String { self.receivedResponse = response } else { @@ -44,4 +51,15 @@ public class DemoAPICallback: Callback { expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: CollectCallback { + CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: RevealCallback { + RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift new file mode 100644 index 00000000..64a125f9 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Minimal URLProtocol-based network mock. Register via URLProtocol.registerClass(MockURLProtocol.self) +// before the request fires (URLSession(configuration: .default) consults globally registered +// protocol classes), and unregister afterward. Used for tests that need to exercise real +// URLSession.dataTask call sites (e.g. FlowVaultInsertAPICallback's insert+update merge), where +// bypassing the network layer entirely (as most other tests do via processResponse(data:response:error:)) +// wouldn't exercise the merge logic itself. + +import Foundation + +final class MockURLProtocol: URLProtocol { + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + client?.urlProtocol(self, didFailWithError: NSError(domain: "MockURLProtocol", code: -1, userInfo: [NSLocalizedDescriptionKey: "No requestHandler configured"])) + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index 40bbe132..e8af5ae7 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -3,134 +3,174 @@ */ import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore final class skyflow_iOS_collectUtilTests: XCTestCase { - var collectCallback: CollectAPICallback! = nil + var collectCallback: FlowVaultCollectAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] - + override func setUp() { - self.collectCallback = CollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultCollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: ICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions()) } - + func testBuildFieldsDict() { let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] let result = self.collectCallback.buildFieldsDict(dict: dict) XCTAssertEqual(dict["key"] as! String, result["key"] as! String) XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) } - + func testOnSuccessInvalidUrl() { let expectation = XCTestExpectation(description: "Invalid URL should trigger failure") let callback = DemoAPICallback(expectation: expectation) self.collectCallback.apiClient.vaultURL = "Invalid url" self.collectCallback.callback = callback - + self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - - let result = callback.data["errors"] as! [[String: Any]] - let errorObject = result[0]["error"] as! [String: Any] + + let errorObject = callback.data["error"] as! [String: Any] let msg = errorObject["message"] as! String XCTAssert(msg.contains("unsupported URL")) } - + func testGetRequestSession() { let url = URL(string: "https://example.org")! do { let (request, session) = try self.collectCallback.getRequestSession(url: url) - + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") // From DemoTokenProvider() + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] let records = body["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["fields"] as! [String: String], ["field": "value"]) - XCTAssertEqual(records[0]["method"] as! String, "POST") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) XCTAssertEqual(records[0]["tableName"] as! String, "table") - + } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) - - XCTAssertEqual(result as! [String: [[String: String]]], ["records": [ - [ - "table": "table", - "skyflow_id": "SID" - ] - ]]) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["tokens"]) } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponseWithTokens() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - self.collectCallback.options = ICOptions() - + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] + self.collectCallback.options = FlowVaultICOptions() + + do { + let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + let fields = records[0]["fields"] as! [String: Any] + let fieldTokens = fields["field"] as! [[String: Any]] + XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGetCollectResponseWithHashedData() { + let response: [String: Any] = ["records": [[ + "skyflowID": "SID", + "tableName": "table", + "httpCode": 200, + "data": ["field": "value"], + "hashedData": ["field": "hashed-value"] + ]]] + self.collectCallback.options = FlowVaultICOptions() + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - - let expected = ["records": [["table": "table", "fields": ["field": "value", "skyflow_id": "SID"]]]] - - XCTAssertEqual(records.count, expected["records"]!.count) - XCTAssertEqual(records[0]["table"] as! String, expected["records"]![0]["table"] as! String) - XCTAssertEqual(records[0]["fields"] as! [String: String], expected["records"]![0]["fields"] as! [String: String]) - - + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) + XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) as! [String: [[String: String]]] - XCTAssertEqual(processedData, ["records": [["skyflow_id": "SID", "table": "table"]]]) - + + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let records = processedData["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponseError() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + // A genuine connection/network-level error (URLSession error, no HTTP response reached) + // should not throw - it returns a top-level {"error": {...}} dict directly. + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + do { - let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) - let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) - as! [String: [String: String]] + let processedData = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = processedData["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "The Internet connection appears to be offline.") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testGetCollectResponseBodyWithMalformedTopLevelJSON() { + // A response body that parses as valid JSON but isn't a top-level object (e.g. a bare + // array or scalar) must not crash - it should fall back to an empty result. + do { + let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: arrayData) + XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) } catch { + XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } } - + func testProcessResponseFailure() { let response = ["error": ["message": "Internal Server Error"]] do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - + // XCTFail("Not throwing on Api Error") do { var res = try self.collectCallback.processResponse(data: data, response: response, error: nil) @@ -143,19 +183,19 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testCollectInvalidBearerToken() { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) let client = Client(Configuration(vaultID: "id", vaultURL: "https://www.skyflow.com", tokenProvider: DemoTokenProvider())) let container = client.container(type: ContainerType.COLLECT) - let input = CollectElementInput(table: "table", column: "column", type: .EXPIRATION_YEAR) + let input = CollectElementInput(tableName: "table", column: "column", type: .EXPIRATION_YEAR) let element = container?.create(input: input) - + UIWindow().addSubview(element!) - - container?.collect(callback: callback) - + + container?.collect(callback: callback.asCollectCallback) + wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } @@ -167,68 +207,34 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertEqual(device.systemName + "@" + device.systemVersion, deviceInfo["sdk_client_os_details"] as! String) } func testDeviceDetails() { - + let deviceDetails = FetchMetrices().getDeviceDetails() - + XCTAssertNotNil(deviceDetails["device"]) XCTAssertNotNil(deviceDetails["os_details"]) XCTAssertNotNil(deviceDetails["sdk_name_version"]) - + if let device = deviceDetails["device"] as? String { XCTAssertFalse(device.isEmpty) } else { XCTAssertTrue(deviceDetails["device"] as! String == "") } - + if let osDetails = deviceDetails["os_details"] as? String { XCTAssertFalse(osDetails.isEmpty) } else { XCTAssertTrue(deviceDetails["os_details"] as! String == "") } - + if let sdkNameVersion = deviceDetails["sdk_name_version"] as? String { XCTAssertFalse(sdkNameVersion.isEmpty) } else { XCTAssertTrue(deviceDetails["sdk_name_version"] as! String == "") } } - func testUpdateNewFlow() { - let expectation = XCTestExpectation(description: "Update new flow should succeed and merge response") - let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) - // Simulate a successful update response - let responseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) - let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - do { - let processed = try collectCallback.processUpdateResponse(data: responseData, response: urlResponse, error: nil, table: "table") - let records = processed["records"] as! [[String: Any]] - print("records after processing update response", records) - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as? String, "table") - let fields = records[0]["fields"] as? [String: String] - XCTAssertEqual((fields?["skyflow_id"] ?? "") as String, "id1") - XCTAssertEqual(fields?["field"] as? String, "newValue") - } catch { - XCTFail("Update flow failed: \(error)") - } - } - func testPartialInsertAndUpdateScenario() { - let expectation = XCTestExpectation(description: "Partial insert and update scenario should succeed") + + func testOnlyInsertSuccess() { + let expectation = XCTestExpectation(description: "Only insert records should succeed") let callback = DemoAPICallback(expectation: expectation) let insertRecord: [String: Any] = [ @@ -236,343 +242,404 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { "fields": ["field": "value"] ] - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": [insertRecord]], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertResponseDict: [String: Any] = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) + let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let insertRecords = processedInsert["records"] as! [[String: Any]] - let updateRecords = processedUpdate["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - let ifields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(ifields?["skyflow_id"] as? String, "SID") - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + XCTAssertEqual(insertRecords[0]["tableName"] as? String, "table") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") + let fields = insertRecords[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["field"] as? String, "value") } catch { - XCTFail("Partial insert and update scenario failed: \(error)") + XCTFail("Insert scenario failed: \(error)") } } - func testOnlyInsertSuccess() { - let expectation = XCTestExpectation(description: "Only insert records should succeed") + + func testInsertPartialFailure() { + let expectation = XCTestExpectation(description: "Partial insert failure should populate errors array") let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] + let insertRecords: [[String: Any]] = [ + ["table": "table", "fields": ["field": "value"]], + ["table": "table", "fields": ["field": "value2"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]], + ["tableName": "table", "error": "insert failed", "httpCode": 400] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let insertRecords = processedInsert["records"] as! [[String: Any]] + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - XCTAssertEqual(insertRecords.count, 1) - let fields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"] as? String, "SID") + XCTAssertEqual(records.count, 2) + XCTAssertNil(records[0]["error"]) + XCTAssertEqual(records[1]["error"] as? String, "insert failed") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert scenario failed: \(error)") + XCTFail("Partial insert failure scenario failed: \(error)") } } - func testOnlyUpdateSuccess() { - let expectation = XCTestExpectation(description: "Only update records should succeed") + func testInsertFullFailureWithNon2xxOuterStatus() { + let expectation = XCTestExpectation(description: "Full batch failure with 400 outer status should still parse per-record errors") let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let insertRecords: [[String: Any]] = [ + ["table": "table", "fields": ["field": "value"]], + ["table": "table", "fields": ["field": "value2"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""], + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) do { - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 400) } catch { - XCTFail("Update scenario failed: \(error)") + XCTFail("Full failure scenario should not throw: \(error)") } } - func testInsertAndUpdateSuccess() { - let expectation = XCTestExpectation(description: "Insert and update records should succeed") + func testInsertPartialFailureWith207OuterStatus() { + let expectation = XCTestExpectation(description: "Partial failure with 207 outer status should parse per-record errors") let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] - - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let insertRecords: [[String: Any]] = [ + ["table": "table2", "fields": ["address": "dedede", "gender": "sagar"]], + ["table": "table", "fields": ["field": "value"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful insert response -// let insertResponseDict: [String: Any] = [ -// "records": [["skyflow_id": "inserted_id"]] -// ] - let insertResponseDict = ["responses": [["records": [["skyflow_id": "inserted_id"]]], ["fields": ["field": "value"]]]] - - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": "b187b5b6-28b4-4881-93fa-4ef10e30b20e", "tokens": ["address": [["token": "dedwim", "tokenGroupName": "deterministic_string"]]], + "data": ["address": "dedede", "gender": "sagar"], "hashedData": [:], "error": NSNull(), "httpCode": 200, "tableName": "table2"], + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - - let insertRecords = processedInsert["records"] as! [[String: Any]] - let updateRecords = processedUpdate["records"] as! [[String: Any]] + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - XCTAssertEqual(insertRecords.count, 1) - var ifields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(ifields?["skyflow_id"] as? String, "inserted_id") - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert and update scenario failed: \(error)") + XCTFail("Partial (207) scenario should not throw: \(error)") } } - func testInsertSuccessAndUpdateFailure() { - let expectation = XCTestExpectation(description: "Insert success and update failure") - let callback = DemoAPICallback(expectation: expectation) + func testUpdateSuccessResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": "f30c8ccf-7e86-46b4-be74-0b2db44e4b87", + "tokens": { + "email": [{"token": "a@ehmw.aqk", "tokenGroupName": "nondeterministic_string"}], + "name": [{"token": "sagwm", "tokenGroupName": "deterministic_string"}], + "passport": [{"token": "21284054160", "tokenGroupName": "deterministic_string"}] + }, + "data": {"email": "b@demo.com", "name": "sagar", "passport": "21212121212"}, + "hashedData": {}, + "error": null, + "httpCode": 200, + "tableName": "table1" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + self.collectCallback.options = FlowVaultICOptions() - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table1") + XCTAssertEqual(records[0]["skyflowID"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) + let nameTokens = fields["name"] as! [[String: Any]] + XCTAssertEqual(nameTokens[0]["token"] as? String, "sagwm") + } catch { + XCTFail("Update success response should not throw: \(error)") + } + } - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) + func testUpdateFullFailureResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + }, + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) - // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "inserted_id"]]], ["fields": ["field": "value"]]]] - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] - // Simulate a failed update response - let updateError = NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "Update failed"]) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + } catch { + XCTFail("Update full failure response should not throw: \(error)") + } + } - do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let insertRecords = processedInsert["records"] as! [[String: Any]] + func testUpdatePartialFailureResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": "b187b5b6-28b4-4881-93fa-4ef10e30b20e", + "tokens": { + "address": [{"token": "dedwim", "tokenGroupName": "deterministic_string"}], + "gender": [{"token": "sagwm", "tokenGroupName": "deterministic_string"}] + }, + "data": {"address": "dedede", "gender": "sagar"}, + "hashedData": {}, + "error": null, + "httpCode": 200, + "tableName": "table2" + }, + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) + self.collectCallback.options = FlowVaultICOptions() - XCTAssertEqual(insertRecords.count, 1) - var fields = insertRecords[0]["fields"] as? [String: Any] - XCTAssertEqual(fields?["skyflow_id"] as? String, "inserted_id") + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - let insertResponse = try collectCallback.processUpdateResponse(data: nil, response: nil, error: updateError, table: "table") as! [String: [String: String]] - XCTAssertEqual(insertResponse, ["error": ["message": "Update failed"]]) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert success and update failure scenario failed: \(error)") + XCTFail("Update partial response should not throw: \(error)") } } - func testInsertFailureAndUpdateSuccess() { - let expectation = XCTestExpectation(description: "Insert failure and update success") - let callback = DemoAPICallback(expectation: expectation) + // The tests above all call processResponse(data:response:error:) directly, which only + // exercises parsing a single HTTP response body. When a request has BOTH inserts and + // updates, FlowVaultInsertAPICallback.onSuccess fires two REAL network calls (v2/records/insert + // and v2/records/update) and merges both results via a DispatchGroup - that merge logic can + // only be exercised by going through onSuccess with real URLSession dispatch, hence the + // URLProtocol mock here. + func testInsertAndUpdateResponsesMergeIntoOneCollectResponse() { + let mockConfiguration = URLSessionConfiguration.ephemeral + mockConfiguration.protocolClasses = [MockURLProtocol.self] + let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration + FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + + MockURLProtocol.requestHandler = { request in + let url = request.url!.absoluteString + let body: [String: Any] + if url.contains("v2/records/insert") { + body = ["records": [["skyflowID": "insertedId", "tableName": "cards", "tokens": ["card_number": "value"], "httpCode": 200]]] + } else if url.contains("v2/records/update") { + body = ["records": [["skyflowID": "id1", "tableName": "persons", "tokens": [:], "httpCode": 200]]] + } else { + XCTFail("Unexpected URL: \(url)") + body = ["records": []] + } + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] + let expectation = XCTestExpectation(description: "Merged insert+update response") + let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let records: [String: Any] = [ + "records": [["table": "cards", "fields": ["card_number": "4111"]]], + "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] ] - let collectCallback = CollectAPICallback( - callback: callback, + let insertApiCallback = FlowVaultInsertAPICallback( + callback: callback.asCollectCallback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: records, + options: FlowVaultICOptions(), contextOptions: ContextOptions() ) + // onSuccess's parameter here is the bearer token from TokenAPICallback - unused by this + // method's own logic (it reads self.records instead), so any value triggers the real flow. + insertApiCallback.onSuccess("dummy-token") - // Simulate a failed insert response - let insertErrorResponseDict: [String: Any] = [ - "error": ["message": "Insert failed"] - ] - let insertErrorResponseData = try! JSONSerialization.data(withJSONObject: insertErrorResponseDict, options: .fragmentsAllowed) - let insertErrorUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + wait(for: [expectation], timeout: 10.0) - do { - let insertProccessed = try collectCallback.processResponse(data: insertErrorResponseData, response: insertErrorUrlResponse, error: nil) as! [String: [String: Any]] - XCTAssertEqual(insertProccessed["error"]?["message"] as? String, "Insert failed") - XCTAssertEqual(insertProccessed["error"]?["code"] as? Int, 404) - - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") - } catch { - XCTFail("Insert failure and update success scenario failed: \(error)") + guard let collectResponse = callback.collectResponse else { + XCTFail("Expected a merged CollectResponse, got: receivedResponse=\(callback.receivedResponse) data=\(callback.data)") + return } + XCTAssertEqual(collectResponse.records.count, 2) + XCTAssertTrue(collectResponse.records.contains { $0.tableName == "cards" && $0.skyflowId == "insertedId" }) + XCTAssertTrue(collectResponse.records.contains { $0.tableName == "persons" && $0.skyflowId == "id1" }) } - func testInsertFailureAndUpdateFailure() { - let expectation = XCTestExpectation(description: "Insert failure and update failure") - let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] + func testInsertSucceedsButUpdateFailsSurfacesAsSkyflowError() { + // One side of the merge fails outright (not a per-record error, a whole-side failure) - + // the merge should surface a proper SkyflowError with the real message, not a + // stringified dump of the internal {"records", "errors"} merge container. + let mockConfiguration = URLSessionConfiguration.ephemeral + mockConfiguration.protocolClasses = [MockURLProtocol.self] + let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration + FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + + MockURLProtocol.requestHandler = { request in + let url = request.url!.absoluteString + if url.contains("v2/records/insert") { + let body: [String: Any] = ["records": [["skyflowID": "insertedId", "tableName": "cards", "tokens": ["card_number": "value"], "httpCode": 200]]] + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } else if url.contains("v2/records/update") { + let body: [String: Any] = ["error": ["message": "Update service unavailable", "httpCode": 503]] + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 503, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } + XCTFail("Unexpected URL: \(url)") + return (HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: "1.1", headerFields: nil)!, Data()) + } - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let expectation = XCTestExpectation(description: "Update-side failure surfaces as SkyflowError") + let callback = DemoAPICallback(expectation: expectation) + + let records: [String: Any] = [ + "records": [["table": "cards", "fields": ["card_number": "4111"]]], + "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] ] - let collectCallback = CollectAPICallback( - callback: callback, + let insertApiCallback = FlowVaultInsertAPICallback( + callback: callback.asCollectCallback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: records, + options: FlowVaultICOptions(), contextOptions: ContextOptions() ) + insertApiCallback.onSuccess("dummy-token") - // Simulate a failed insert response - let insertErrorResponseDict: [String: Any] = [ - "error": ["message": "Insert failed"] - ] - let insertErrorResponseData = try! JSONSerialization.data(withJSONObject: insertErrorResponseDict, options: .fragmentsAllowed) - let insertErrorUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + wait(for: [expectation], timeout: 10.0) - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateErrorResponseDict: [String: Any] = [ - "error": ["message": "Update failed"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateErrorResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + XCTAssertEqual(callback.receivedResponse, "Update service unavailable") + } - do { - let insertProccessed = try collectCallback.processResponse(data: insertErrorResponseData, response: insertErrorUrlResponse, error: nil) as! [String: [String: Any]] - XCTAssertEqual(insertProccessed["error"]?["message"] as? String, "Insert failed") - XCTAssertEqual(insertProccessed["error"]?["code"] as? Int, 404) + func testCollectRecordHttpCodeDefaultsToZeroWhenMissing() { + // httpCode is declared non-optional (Int, not Int?) - verify a dict lacking the key + // doesn't crash and just defaults to 0 instead. + let record = CollectRecord(["tableName": "persons", "skyflowID": "id1"]) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + XCTAssertEqual(record.tableName, "persons") + } - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") as! [String: [String: Any]] + func testCollectRecordErrorDiscriminatesSuccessFromFailure() { + let success = CollectRecord(["tableName": "persons", "httpCode": 200]) + let failure = CollectRecord(["error": "Invalid request", "httpCode": 400]) - XCTAssertEqual(processedUpdate["error"]?["message"] as? String, "Update failed") - XCTAssertEqual(processedUpdate["error"]?["code"] as? Int, 404) + XCTAssertNil(success.error) + XCTAssertEqual(failure.error, "Invalid request") + XCTAssertEqual(failure.httpCode, 400) + } - } catch { - XCTFail("Insert failure and update success scenario failed: \(error)") - } + func testCollectResponseInitReturnsNilForMalformedBody() { + XCTAssertNil(CollectResponse("not a dictionary")) + XCTAssertNil(CollectResponse(["typo": []])) + XCTAssertNil(CollectResponse(["records": "not an array"])) + } + + func testCollectResponseInitParsesValidBody() { + let response = CollectResponse([ + "records": [ + ["tableName": "persons", "skyflowID": "id1", "httpCode": 200], + ["error": "failed", "httpCode": 400] + ] + ]) + XCTAssertEqual(response?.records.count, 2) + XCTAssertNil(response?.records[0].error) + XCTAssertEqual(response?.records[1].error, "failed") } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift new file mode 100644 index 00000000..6ba1fe78 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +import XCTest +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore + +final class skyflow_iOS_cvvMockTests: XCTestCase { + + // MARK: - CVVMockGenerator + + func testGeneratedMockMatchesRequestedLength() { + for length in [3, 4] { + let mock = CVVMockGenerator.generateMockCVV(length: length, actualValue: "000") + XCTAssertEqual(mock.count, length) + } + } + + func testGeneratedMockIsNumeric() { + let mock = CVVMockGenerator.generateMockCVV(length: 4, actualValue: "1234") + XCTAssertNotNil(Int(mock)) + } + + func testGeneratedMockNeverEqualsActualValue() { + // Run repeatedly since generation is random - none of the draws should ever match. + for _ in 0..<200 { + let mock = CVVMockGenerator.generateMockCVV(length: 3, actualValue: "123") + XCTAssertNotEqual(mock, "123") + } + } + + func testGeneratedMockAllowsLeadingZeros() { + // A 3-digit space has only 1000 possibilities; forcing actualValue to something that + // can't collide keeps this deterministic while still exercising the "starts with 0" case + // over many draws. + var sawLeadingZero = false + for _ in 0..<500 where !sawLeadingZero { + let mock = CVVMockGenerator.generateMockCVV(length: 3, actualValue: "999") + if mock.hasPrefix("0") { + sawLeadingZero = true + } + } + XCTAssertTrue(sawLeadingZero) + } + + // MARK: - CVVTokenReplacer.captureCVVMap + + private func makeCVVElement(table: String, column: String, value: String, skyflowId: String? = nil) -> TextField { + let input = CollectElementInput(tableName: table, column: column, type: .CVV, skyflowId: skyflowId) + let field = TextField(input: input.data, options: CollectElementOptions().data, contextOptions: ContextOptions(), elements: []) + field.actualValue = value + return field + } + + private func makeInputFieldElement(table: String, column: String, value: String) -> TextField { + let input = CollectElementInput(tableName: table, column: column, type: .INPUT_FIELD) + let field = TextField(input: input.data, options: CollectElementOptions().data, contextOptions: ContextOptions(), elements: []) + field.actualValue = value + return field + } + + func testCaptureOnlyPicksUpCVVElements() { + let cvv = makeCVVElement(table: "persons", column: "cvv", value: "123") + let name = makeInputFieldElement(table: "persons", column: "name", value: "John") + + let map = CVVTokenReplacer.captureCVVMap(elements: [cvv, name]) + + XCTAssertEqual(map.byTable["persons"]?["cvv"], "123") + XCTAssertNil(map.byTable["persons"]?["name"]) + } + + func testCaptureKeysInsertsByTableAndUpdatesByRecordId() { + let insertCVV = makeCVVElement(table: "persons", column: "cvv", value: "123") + let updateCVV = makeCVVElement(table: "persons", column: "cvv", value: "456", skyflowId: "SID1") + + let map = CVVTokenReplacer.captureCVVMap(elements: [insertCVV, updateCVV]) + + XCTAssertEqual(map.byTable["persons"]?["cvv"], "123") + XCTAssertEqual(map.byRecordId["SID1"]?["cvv"], "456") + } + + func testCaptureFirstElementWinsOnDuplicateColumn() { + // Mirrors FlowVaultCollectRequestBody's own dedup rule (e.g. ElementValueMatchRule): + // when two elements target the same column, only the first one's value reaches the vault. + let first = makeCVVElement(table: "persons", column: "cvv", value: "111") + let second = makeCVVElement(table: "persons", column: "cvv", value: "222") + + let map = CVVTokenReplacer.captureCVVMap(elements: [first, second]) + + XCTAssertEqual(map.byTable["persons"]?["cvv"], "111") + } + + func testCaptureStillCapturesEmptyValue() { + // An optional CVV element left blank still submits "" to the vault and can get a real + // token back - it must still be captured so that token gets masked too. + let cvv = makeCVVElement(table: "persons", column: "cvv", value: "") + let map = CVVTokenReplacer.captureCVVMap(elements: [cvv]) + XCTAssertEqual(map.byTable["persons"]?["cvv"], "") + } + + func testReplaceFlatColumnWithEmptyEnteredValueBecomesEmptyString() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": ""]], byRecordId: [:]) + let records: [[String: Any]] = [[ + "tableName": "persons", + "fields": [ + "cvv": [["token": "real-token-for-empty-value", "tokenGroupName": "deterministic"]], + "name": [["token": "name-token", "tokenGroupName": "deterministic"]] + ] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + let fields = result[0]["fields"] as! [String: Any] + let cvvEntries = fields["cvv"] as! [[String: Any]] + let nameEntries = fields["name"] as! [[String: Any]] + + // A real (long) vault token no longer leaks through unmasked just because the field + // was left blank - it's replaced with "", never a generated mock (generating one for a + // length-0 entered value would never terminate, see CVVMockGenerator's guard). + XCTAssertEqual(cvvEntries[0]["token"] as! String, "") + // Sibling column untouched. + XCTAssertEqual(nameEntries[0]["token"] as! String, "name-token") + } + + func testReplaceNestedColumnWithEmptyEnteredValueBecomesEmptyString() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["address.pincode": ""]], byRecordId: [:]) + let records: [[String: Any]] = [[ + "tableName": "persons", + "fields": [ + "address": [ + ["token": "whole-column-token", "tokenGroupName": "deterministic"], + ["path": "pincode", "token": "real-pincode-token", "tokenGroupName": "deterministic"], + ["path": "city", "token": "city-token", "tokenGroupName": "deterministic"] + ] + ] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + let addressEntries = (result[0]["fields"] as! [String: Any])["address"] as! [[String: Any]] + + let wholeColumn = addressEntries.first { $0["path"] == nil }! + let pincode = addressEntries.first { $0["path"] as? String == "pincode" }! + let city = addressEntries.first { $0["path"] as? String == "city" }! + + // Only the "pincode" leaf becomes "" - parent/sibling entries are untouched. + XCTAssertEqual(pincode["token"] as! String, "") + XCTAssertEqual(wholeColumn["token"] as! String, "whole-column-token") + XCTAssertEqual(city["token"] as! String, "city-token") + } + + func testGeneratedMockWithZeroLengthReturnsEmptyStringWithoutLooping() { + // Guards against the hazard directly: length 0 with actualValue "" would make the + // "regenerate until different" loop never terminate if this guard weren't there, + // since every candidate is "" and "" always equals the empty actualValue. + let mock = CVVMockGenerator.generateMockCVV(length: 0, actualValue: "") + XCTAssertEqual(mock, "") + } + + private func makePINElement(table: String, column: String, value: String) -> TextField { + let input = CollectElementInput(tableName: table, column: column, type: .PIN) + let field = TextField(input: input.data, options: CollectElementOptions().data, contextOptions: ContextOptions(), elements: []) + field.actualValue = value + return field + } + + // A PIN element is never masked, even on a column literally named "pincode" (a postal-code + // subfield, unrelated to ElementType.PIN) or sharing a table with a real CVV element. + func testCaptureAndReplaceNeverTouchPINElements() { + let cvv = makeCVVElement(table: "nested", column: "card.cvv", value: "123") + let pin = makePINElement(table: "nested", column: "address.pincode", value: "5000") + + let map = CVVTokenReplacer.captureCVVMap(elements: [cvv, pin]) + XCTAssertNil(map.byTable["nested"]?["address.pincode"]) + XCTAssertEqual(map.byTable["nested"]?["card.cvv"], "123") + + let records: [[String: Any]] = [[ + "tableName": "nested", + "fields": [ + "card": [["path": "cvv", "token": "real-cvv-token", "tokenGroupName": "deterministic"]], + "address": [["path": "pincode", "token": "real-pincode-token", "tokenGroupName": "deterministic"]] + ] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: map) + let fields = result[0]["fields"] as! [String: Any] + let cardEntries = fields["card"] as! [[String: Any]] + let addressEntries = fields["address"] as! [[String: Any]] + + XCTAssertNotEqual(cardEntries[0]["token"] as! String, "real-cvv-token") + // PIN's "pincode" token is untouched. + XCTAssertEqual(addressEntries[0]["token"] as! String, "real-pincode-token") + } + + // MARK: - CVVTokenReplacer.replaceCVVTokens + + func testReplaceFlatColumnSingleTokenGroup() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": "123"]], byRecordId: [:]) + let records: [[String: Any]] = [[ + "tableName": "persons", + "skyflowID": "SID1", + "fields": [ + "cvv": [["token": "real-token", "tokenGroupName": "deterministic"]], + "name": [["token": "name-token", "tokenGroupName": "deterministic"]] + ] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + let fields = result[0]["fields"] as! [String: Any] + let cvvEntries = fields["cvv"] as! [[String: Any]] + let nameEntries = fields["name"] as! [[String: Any]] + + XCTAssertEqual(cvvEntries.count, 1) + XCTAssertNotEqual(cvvEntries[0]["token"] as! String, "real-token") + XCTAssertEqual((cvvEntries[0]["token"] as! String).count, 3) + // Non-CVV column untouched. + XCTAssertEqual(nameEntries[0]["token"] as! String, "name-token") + } + + func testReplaceFlatColumnMultipleTokenGroups() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": "1234"]], byRecordId: [:]) + let records: [[String: Any]] = [[ + "tableName": "persons", + "fields": [ + "cvv": [ + ["token": "det-token", "tokenGroupName": "deterministic"], + ["token": "nondet-token", "tokenGroupName": "nondeterministic"] + ] + ] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + let cvvEntries = (result[0]["fields"] as! [String: Any])["cvv"] as! [[String: Any]] + + XCTAssertEqual(cvvEntries.count, 2) + let mock1 = cvvEntries[0]["token"] as! String + let mock2 = cvvEntries[1]["token"] as! String + // Both token-group entries for the same column get the same mock. + XCTAssertEqual(mock1, mock2) + XCTAssertNotEqual(mock1, "1234") + } + + func testReplaceNestedColumnOnlyTouchesMatchingPath() { + let cvvMap = CVVCaptureMap(byTable: ["nested": ["address.pincode": "9876"]], byRecordId: [:]) + let records: [[String: Any]] = [[ + "tableName": "nested", + "fields": [ + "address": [ + ["token": "whole-column-token", "tokenGroupName": "deterministic"], + ["path": "pincode", "token": "pincode-token", "tokenGroupName": "deterministic"], + ["path": "city", "token": "city-token", "tokenGroupName": "deterministic"] + ] + ] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + let addressEntries = (result[0]["fields"] as! [String: Any])["address"] as! [[String: Any]] + + let wholeColumn = addressEntries.first { $0["path"] == nil }! + let pincode = addressEntries.first { $0["path"] as? String == "pincode" }! + let city = addressEntries.first { $0["path"] as? String == "city" }! + + // Only the "pincode" subfield is mocked. + XCTAssertEqual(wholeColumn["token"] as! String, "whole-column-token") + XCTAssertNotEqual(pincode["token"] as! String, "pincode-token") + XCTAssertEqual(city["token"] as! String, "city-token") + } + + func testReplaceMatchesUpdateByRecordIdBeforeInsertByTable() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": "111"]], byRecordId: ["SID1": ["cvv": "222"]]) + let records: [[String: Any]] = [[ + "tableName": "persons", + "skyflowID": "SID1", + "fields": ["cvv": [["token": "real-token", "tokenGroupName": "deterministic"]]] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + let cvvEntries = (result[0]["fields"] as! [String: Any])["cvv"] as! [[String: Any]] + + // Matched via byRecordId (length of captured "222" is 3), not byTable's "111". + XCTAssertEqual((cvvEntries[0]["token"] as! String).count, 3) + } + + func testReplaceLeavesHashedDataAndErrorRecordsUntouched() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": "123"]], byRecordId: [:]) + let records: [[String: Any]] = [ + [ + "tableName": "persons", + "fields": ["cvv": [["token": "real-token", "tokenGroupName": "deterministic"]]], + "hashedData": ["cvv": [["data": "hash", "hashName": "sha256"]]] + ], + [ + "error": "some error", + "tableName": "persons" + ] + ] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: cvvMap) + + let hashedData = result[0]["hashedData"] as! [String: Any] + let hashEntries = hashedData["cvv"] as! [[String: Any]] + XCTAssertEqual(hashEntries[0]["data"] as! String, "hash") + + XCTAssertEqual(result[1]["error"] as! String, "some error") + XCTAssertNil(result[1]["fields"]) + } + + func testReplaceIsNoOpWhenNoCVVCaptured() { + let records: [[String: Any]] = [[ + "tableName": "persons", + "fields": ["cvv": [["token": "real-token", "tokenGroupName": "deterministic"]]] + ]] + + let result = CVVTokenReplacer.replaceCVVTokens(in: records, cvvMap: CVVCaptureMap()) + let cvvEntries = (result[0]["fields"] as! [String: Any])["cvv"] as! [[String: Any]] + XCTAssertEqual(cvvEntries[0]["token"] as! String, "real-token") + } + + // MARK: - CVVMaskingCallback + + func testMaskingCallbackMasksOnSuccessRecords() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": "123"]], byRecordId: [:]) + let expectation = XCTestExpectation(description: "onSuccess forwarded") + let demo = DemoAPICallback(expectation: expectation) + let masking = CVVMaskingCallback(cvvMap: cvvMap, wrapping: demo) + + masking.onSuccess([ + "records": [[ + "tableName": "persons", + "fields": ["cvv": [["token": "real-token", "tokenGroupName": "deterministic"]]] + ]] + ]) + + wait(for: [expectation], timeout: 5.0) + let records = demo.data["records"] as! [[String: Any]] + let cvvEntries = (records[0]["fields"] as! [String: Any])["cvv"] as! [[String: Any]] + XCTAssertNotEqual(cvvEntries[0]["token"] as! String, "real-token") + } + + func testMaskingCallbackMasksPartialFailureRecords() { + let cvvMap = CVVCaptureMap(byTable: ["persons": ["cvv": "123"]], byRecordId: [:]) + let expectation = XCTestExpectation(description: "onFailure forwarded") + let demo = DemoAPICallback(expectation: expectation) + let masking = CVVMaskingCallback(cvvMap: cvvMap, wrapping: demo) + + masking.onFailure([ + "records": [[ + "tableName": "persons", + "fields": ["cvv": [["token": "real-token", "tokenGroupName": "deterministic"]]] + ]], + "errors": [["error": "some other record failed"]] + ]) + + wait(for: [expectation], timeout: 5.0) + let records = demo.data["records"] as! [[String: Any]] + let cvvEntries = (records[0]["fields"] as! [String: Any])["cvv"] as! [[String: Any]] + XCTAssertNotEqual(cvvEntries[0]["token"] as! String, "real-token") + } + + // Note: a true end-to-end test through CollectContainer.collect()/ComposableContainer.collect() + // (real network dispatch) isn't reachable from a unit test here: FlowVaultCollectAPICallback + // always builds a fresh URLSession(configuration: .default), and URLProtocol.registerClass + // only reliably intercepts URLSession.shared, not ad-hoc .default sessions - confirmed by this + // failing against the real network (github.com/.../example.org) rather than the mock handler. + // Making that interceptable would require adding a test-only seam to production SDK code + // (e.g. FlowVaultInsertAPICallback's injectable urlSessionConfiguration), which is out of + // scope here. The wiring itself (cvvMap capture + CVVMaskingCallback insertion in + // CollectContainer.swift/ComposableContainer.swift) is two lines per container and is + // exercised for real via the NormalTesting sample app's CVV Mock Scenarios screen against a + // live vault; the masking logic itself is fully covered above without needing the network. +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index e3becb93..3640d453 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -3,153 +3,192 @@ */ import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore final class skyflow_iOS_insertUtilTests: XCTestCase { - var collectCallback: InsertAPICallback! = nil + var collectCallback: FlowVaultInsertAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] - + override func setUp() { - self.collectCallback = InsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultInsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: ICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions()) } - + func testBuildFieldsDict() { let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] let result = self.collectCallback.buildFieldsDict(dict: dict) XCTAssertEqual(dict["key"] as! String, result["key"] as! String) XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) } - + func testOnSuccessInvalidUrl() { let expectation = XCTestExpectation(description: "Invalid URL should trigger failure") let callback = DemoAPICallback(expectation: expectation) self.collectCallback.apiClient.vaultURL = "Invalid url" self.collectCallback.callback = callback - + self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - - let result = callback.receivedResponse - XCTAssert(result.contains("unsupported URL")) + + let errorObject = callback.data["error"] as! [String: Any] + let msg = errorObject["message"] as! String + XCTAssert(msg.contains("unsupported URL")) } - + func testGetRequestSession() { let url = URL(string: "https://example.org")! do { let (request, session) = try self.collectCallback.getRequestSession(url: url) - + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") // From DemoTokenProvider() + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] let records = body["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["fields"] as! [String: String], ["field": "value"]) - XCTAssertEqual(records[0]["method"] as! String, "POST") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) XCTAssertEqual(records[0]["tableName"] as! String, "table") - + } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) - - XCTAssertEqual(result as! [String: [[String: String]]], ["records": [ - [ - "table": "table", - "skyflow_id": "SID" - ] - ]]) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponseWithTokens() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - self.collectCallback.options = ICOptions() - + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] + self.collectCallback.options = FlowVaultICOptions() + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - - let expected = ["records": [["table": "table", "fields": ["field": "value", "skyflow_id": "SID"]]]] - - XCTAssertEqual(records.count, expected["records"]!.count) - XCTAssertEqual(records[0]["table"] as! String, expected["records"]![0]["table"] as! String) - XCTAssertEqual(records[0]["fields"] as! [String: String], expected["records"]![0]["fields"] as! [String: String]) - - + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + let fields = records[0]["fields"] as! [String: Any] + let fieldTokens = fields["field"] as! [[String: Any]] + XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") } catch { XCTFail(error.localizedDescription) } } - + + func testGetCollectResponseWithHashedData() { + let response: [String: Any] = ["records": [[ + "skyflowID": "SID", + "tableName": "table", + "httpCode": 200, + "data": ["field": "value"], + "hashedData": ["field": "hashed-value"] + ]]] + self.collectCallback.options = FlowVaultICOptions() + + do { + let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) + XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) + } catch { + XCTFail(error.localizedDescription) + } + } + func testProcessResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) as! [String: [[String: String]]] - XCTAssertEqual(processedData, ["records": [["skyflow_id": "SID", "table": "table"]]]) - + + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let records = processedData["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponseError() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + // A genuine connection/network-level error (URLSession error, no HTTP response reached) + // should not throw - it returns a top-level {"error": {...}} dict directly. + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + do { - let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) - let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) as! [String: [[String: String]]] - XCTFail("Should have thrown error") - + let processedData = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = processedData["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "The Internet connection appears to be offline.") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testGetCollectResponseBodyWithMalformedTopLevelJSON() { + // A response body that parses as valid JSON but isn't a top-level object (e.g. a bare + // array or scalar) must not crash - it should fall back to an empty result. + do { + let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: arrayData) + XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) } catch { + XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } } - + func testProcessResponseFailure() { let response = ["error": ["message": "Internal Server Error"]] do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - - try self.collectCallback.processResponse(data: data, response: response, error: nil) - XCTFail("Not throwing on Api Error") - + + let res = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let message = (res["error"] as! [String: Any])["message"] as! String + XCTAssertEqual(message, "Internal Server Error - request-id: RID") } catch { - XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") + XCTFail("Should not throw on Api Error: \(error)") } } - + func testCollectInvalidBearerToken() { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) let client = Client(Configuration(vaultID: "id", vaultURL: "https://www.skyflow.com", tokenProvider: DemoTokenProvider())) let container = client.container(type: ContainerType.COLLECT) - let input = CollectElementInput(table: "table", column: "column", type: .EXPIRATION_YEAR) + let input = CollectElementInput(tableName: "table", column: "column", type: .EXPIRATION_YEAR) let element = container?.create(input: input) - + UIWindow().addSubview(element!) - - container?.collect(callback: callback) - + + container?.collect(callback: callback.asCollectCallback) + wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } @@ -161,25 +200,25 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { XCTAssertEqual(device.systemName + "@" + device.systemVersion, deviceInfo["sdk_client_os_details"] as! String) } func testDeviceDetails() { - + let deviceDetails = FetchMetrices().getDeviceDetails() - + XCTAssertNotNil(deviceDetails["device"]) XCTAssertNotNil(deviceDetails["os_details"]) XCTAssertNotNil(deviceDetails["sdk_name_version"]) - + if let device = deviceDetails["device"] as? String { XCTAssertFalse(device.isEmpty) } else { XCTAssertTrue(deviceDetails["device"] as! String == "") } - + if let osDetails = deviceDetails["os_details"] as? String { XCTAssertFalse(osDetails.isEmpty) } else { XCTAssertTrue(deviceDetails["os_details"] as! String == "") } - + if let sdkNameVersion = deviceDetails["sdk_name_version"] as? String { XCTAssertFalse(sdkNameVersion.isEmpty) } else { @@ -196,30 +235,70 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { "fields": ["field": "value"] ] - let collectCallback = InsertAPICallback( + let collectCallback = FlowVaultInsertAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] + let insertResponseDict: [String: Any] = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) let insertRecords = processedInsert["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - let fields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"] as? String, "SID") + XCTAssertEqual(insertRecords[0]["tableName"] as? String, "table") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") + let fields = insertRecords[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["field"] as? String, "value") } catch { XCTFail("Insert scenario failed: \(error)") } } + func testInsertFullFailureWithNon2xxOuterStatus() { + let expectation = XCTestExpectation(description: "Full batch failure with 400 outer status should still parse per-record errors") + let callback = DemoAPICallback(expectation: expectation) + + let insertRecord: [String: Any] = [ + "table": "table", + "fields": ["field": "value"] + ] + + let collectCallback = FlowVaultInsertAPICallback( + callback: callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": [insertRecord]], + options: FlowVaultICOptions(additionalFields: nil), + contextOptions: ContextOptions() + ) + + let responseDict: [String: Any] = ["records": [ + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) + + do { + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 400) + } catch { + XCTFail("Full failure scenario should not throw: \(error)") + } + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index f09f9108..523220f3 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -4,12 +4,13 @@ // swiftlint:disable file_length import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore // swiftlint:disable:next type_body_length final class skyflow_iOS_revealUtilTests: XCTestCase { - var revealApiCallback: RevealAPICallback! = nil + var revealApiCallback: FlowVaultRevealAPICallback! = nil var revealValueCallback: RevealValueCallback! = nil var expectation: XCTestExpectation! = nil var callback: DemoAPICallback! = nil @@ -20,7 +21,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { override func setUp() { self.expectation = XCTestExpectation() self.callback = DemoAPICallback(expectation: self.expectation) - self.revealApiCallback = RevealAPICallback(callback: self.callback, + self.revealApiCallback = FlowVaultRevealAPICallback(callback: self.callback, apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), connectionUrl: "", records: [], @@ -40,7 +41,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { func testOnSuccessInvalidUrl() { self.revealApiCallback.connectionUrl = "invalid url" - let record = RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue) + let record = RevealRequestRecord(token: "token") self.revealApiCallback.records = [record] self.revealApiCallback.onSuccess("token") wait(for: [self.expectation], timeout: 10.0) @@ -48,111 +49,178 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["error"]?.localizedDescription, "unsupported URL") } - + func testGetRequestSession() { - self.revealApiCallback.connectionUrl = "https://www.example.org" - - let (request, session) = self.revealApiCallback.getRequestSession() - XCTAssertEqual(request.url?.absoluteString, "https://www.example.org/detokenize") - XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json; utf-8") - XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") - XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") - } - - func testRevealRequestBody() { - let record = RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue) + let url = URL(string: "https://www.example.org")! + do { - let result = try self.revealApiCallback.getRevealRequestBody(record: record) - - if let jsonObject = try? JSONSerialization.jsonObject(with: result, options: []) as? [String: Any] { - let key = jsonObject.keys - let values = jsonObject.values - XCTAssertTrue(key.contains("detokenizationParameters")) - } else { - print("Failed to convert JSON data into a JSON object.") - } - XCTAssertNotNil(result) + let (request, session) = try self.revealApiCallback.getRequestSession(url: url) + XCTAssertEqual(request.url?.absoluteString, "https://www.example.org") + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") } catch { XCTFail(error.localizedDescription) } } - + + func testConstructV2DetokenizeRequestBody() { + self.revealApiCallback.records = [RevealRequestRecord(token: "token1"), RevealRequestRecord(token: "token2")] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: self.revealApiCallback.records) + + XCTAssertEqual(result["vaultID"] as! String, "vault123") + XCTAssertEqual(result["tokens"] as! [String], ["token1", "token2"]) + XCTAssertNil(result["tokenGroupRedactions"]) + } + + func testConstructV2DetokenizeRequestBodyWithTokenGroupRedactions() { + let records = [RevealRequestRecord(token: "token1")] + let tokenGroupRedactions = [TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED")] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + + let redactions = result["tokenGroupRedactions"] as! [[String: Any]] + XCTAssertEqual(redactions.count, 1) + XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[0]["redaction"] as? String, "MASKED") + } + func testProcessResponseError() { let revealedResponse = ["key": "value"] let serverError = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Internal Server Error"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: nil, error: serverError) + _ = try self.revealApiCallback.processResponse(data: responseData, response: nil, error: serverError) XCTFail("Not throwing on http error") } catch { XCTAssertEqual(error.localizedDescription, serverError.localizedDescription) } } - + func testProcessResponseBadCode() { let revealedResponse = ["error": ["message": "Internal Server Error"]] let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - let (success, failure) = try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: httpResponse, error: nil) - XCTAssertNil(success) - XCTAssertNotNil(failure) - XCTAssertEqual(failure?.error.localizedDescription, "Internal Server Error - request-id: RID") + _ = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + XCTFail("Not throwing on http error") } catch { - XCTFail(error.localizedDescription) + XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testProcessResponseSuccess() { - let revealedResponse = ["records": [["token": "token", "value": "value"]]] + let revealedResponse = ["response": [["token": "token", "value": "value"]]] let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - let (success, failure) = try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: httpResponse, error: nil) - XCTAssertNil(failure) - XCTAssertNotNil(success) - XCTAssertEqual(success?.token_id, "token") - XCTAssertEqual(success?.value, "value") + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "token") + XCTAssertEqual(records[0]["value"] as? String, "value") + XCTAssertNil(records[0]["error"]) } catch { XCTFail(error.localizedDescription) } } - - func testHandleCallbacksSuccess() { - let errorObject = NSError(domain: "", code: 200, userInfo: nil) - let success = [RevealSuccessRecord(token_id: "token", value: "value")] - - self.revealApiCallback.handleCallbacks(success: success, failure: [], isSuccess: true, errorObject: nil) - - wait(for: [self.expectation], timeout: 20.0) - let records = self.callback.data["records"] as! [[String: String]] - - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["value"], "value") - XCTAssertEqual(records[0]["token"], "token") + + func testProcessResponseWithMetadataAndHttpCode() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "token", "value": "value", "httpCode": 200, "metadata": ["tableName": "table", "skyflowID": "SID"]] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let metadata = records[0]["metadata"] as! [String: Any] + XCTAssertEqual(metadata["tableName"] as? String, "table") + XCTAssertEqual(metadata["skyflowID"] as? String, "SID") + } catch { + XCTFail(error.localizedDescription) + } } - - func testHandleCallbacksFailure() { - let failure = RevealErrorRecord(id: "token", error: NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"])) - self.revealApiCallback.handleCallbacks(success: [], failure: [failure], isSuccess: true, errorObject: nil) - - wait(for: [self.expectation], timeout: 20.0) - let errors = self.callback.data["errors"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as! NSError, failure.error) - XCTAssertEqual(errors[0]["token"] as! String, "token") + + func testProcessResponsePartialFailure() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "token1", "value": "value1"], + ["token": "token2", "error": "Invalid Token", "httpCode": 400] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["token"] as? String, "token1") + XCTAssertNil(records[0]["error"]) + XCTAssertEqual(records[1]["token"] as? String, "token2") + XCTAssertEqual(records[1]["error"] as? String, "Invalid Token") + } catch { + XCTFail(error.localizedDescription) + } } - - func testHandleCallbacksError() { - let error = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]) - self.revealApiCallback.handleCallbacks(success: [], failure: [], isSuccess: false, errorObject: error) - - wait(for: [self.expectation], timeout: 20.0) - let errors = self.callback.data["errors"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as! NSError, error) + + func testProcessResponseFullFailureWithNon2xxOuterStatus() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "dedwimm", "value": NSNull(), "tokenGroupName": NSNull(), + "error": "Detokenize failed. Token dedwimm is invalid. Specify a valid token.", + "httpCode": 404, "metadata": NSNull()], + ["token": "femwdmm", "value": NSNull(), "tokenGroupName": NSNull(), + "error": "Detokenize failed. Token femwdmm is invalid. Specify a valid token.", + "httpCode": 404, "metadata": NSNull()] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Detokenize failed. Token dedwimm is invalid. Specify a valid token.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 404) + } catch { + XCTFail("Full failure with 404 outer status should not throw: \(error)") + } } - + + func testProcessResponseInvalidTokenGroupError() { + let revealedResponse: [String: Any] = ["error": [ + "grpc_code": 3, + "http_code": 400, + "message": "Detokenize failed. Token group no is invalid. Specify a valid token group.", + "http_status": "Bad Request", + "details": [] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + _ = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + XCTFail("Should throw on genuine top-level error (invalid token group)") + } catch { + XCTAssertEqual(error.localizedDescription, "Detokenize failed. Token group no is invalid. Specify a valid token group.") + } + } + + func testConstructV2DetokenizeRequestBodyPassesThroughDuplicateTokenGroupRedactions() { + let records = [RevealRequestRecord(token: "token1")] + let tokenGroupRedactions = [ + TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED"), + TokenGroupRedaction(tokenGroupName: "group1", redaction: "PLAIN_TEXT") + ] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + + let redactions = result["tokenGroupRedactions"] as! [[String: Any]] + XCTAssertEqual(redactions.count, 2) + XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[0]["redaction"] as? String, "MASKED") + XCTAssertEqual(redactions[1]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[1]["redaction"] as? String, "PLAIN_TEXT") + } + func testGetTokensToErrors() { let errors = [["token": "1234"], ["token": "4321"]] @@ -162,6 +230,148 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { XCTAssertEqual(result["4321"], "Invalid Token") } + func testRevealValueOnSuccessPureSuccess() { + // Every token revealed, zero errors - a genuine full-success scenario + // (as opposed to the mixed success+error fixtures used elsewhere in this file). + let token1 = "123" + let token2 = "456" + let response: [String: Any] = [ + "records": [ + ["token": token1, "value": "John"], + ["token": token2, "value": "Doe"] + ], + "errors": [] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first"), options: RevealElementOptions()) + let element2 = self.container.create(input: RevealElementInput(token: token2, label: "second"), options: RevealElementOptions()) + + self.revealValueCallback.revealElements = [element1, element2] + self.revealValueCallback.onSuccess(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.allSatisfy { $0["error"] == nil }) + XCTAssertEqual(element1.actualValue, "John") + XCTAssertEqual(element2.actualValue, "Doe") + XCTAssertEqual(element1.errorMessage.text, nil) + XCTAssertEqual(element2.errorMessage.text, nil) + } + + func testRevealValueOnFailurePureFailure() { + // Every token invalid, zero successes - a genuine full-failure scenario. + let token1 = "123" + let token2 = "456" + let response: [String: Any] = [ + "records": [], + "errors": [ + ["token": token1, "error": "Invalid Token"], + ["token": token2, "error": "Invalid Token"] + ] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first")) + let element2 = self.container.create(input: RevealElementInput(token: token2, label: "second")) + + self.revealValueCallback.revealElements = [element1, element2] + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let errors = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(errors.count, 2) + XCTAssertTrue(errors.allSatisfy { $0["error"] != nil }) + XCTAssertEqual(element1.actualValue, nil) + XCTAssertEqual(element2.actualValue, nil) + XCTAssertEqual(element1.errorMessage.text, "Invalid Token") + XCTAssertEqual(element2.errorMessage.text, "Invalid Token") + } + + func testRevealValueOnFailureNetworkErrorHasNoPerTokenErrors() { + // Simulates what FlowVaultRevealAPICallback.callRevealOnFailure produces for a genuine + // network/connection-level failure: no "records", and the error entry has no "token" + // (since the failure isn't scoped to any specific token). Elements should not crash and + // should not show a per-element error message (there's no token to match against), but + // the client callback should still receive the error. + let token1 = "123" + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + let response: [String: Any] = [ + "errors": [["error": networkError]] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + // Not scoped to any specific token, so it's delivered via onFailure (not folded into the + // records array) rather than a per-record error. + XCTAssertNil(self.callback.data["records"]) + XCTAssertEqual(self.callback.receivedResponse, "The Internet connection appears to be offline.") + // No token to match this error against, so the element shows no inline error. + XCTAssertEqual(element1.errorMessage.text, nil) + XCTAssertEqual(element1.actualValue, nil) + } + + func testRevealValueOnFailureWithNonDictionaryError() { + // onFailure(_ error: Any) can in principle be called with something that isn't a + // dictionary at all (the Callback protocol accepts Any). Must not crash, and should + // report an empty response rather than propagating garbage. + let element1 = self.container.create(input: RevealElementInput(token: "123", label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure("not a dictionary") + wait(for: [self.expectation], timeout: 20.0) + + XCTAssertNil(self.callback.data["success"]) + XCTAssertNil(self.callback.data["errors"]) + } + + func testRevealValueOnSuccessWithNonDictionaryResponseBody() { + // Same defensive case for onSuccess: a non-dictionary responseBody must not crash. + let element1 = self.container.create(input: RevealElementInput(token: "123", label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onSuccess(42) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["success"]) + XCTAssertNil(self.callback.data["errors"]) + XCTAssertEqual(element1.actualValue, nil) + } + + func testRevealValueOnSuccessSkipsMalformedRecords() { + // A record missing "token" (or not a dictionary at all) must be skipped, not crash + // the whole reveal for every other token in the same batch. + let goodToken = "123" + let response: [String: Any] = [ + "records": [ + ["token": goodToken, "value": "John"], + ["value": "no token here"], + "not even a dictionary", + ["token": 42, "value": "token is not a String"] + ], + "errors": [] + ] + + let goodElement = self.container.create(input: RevealElementInput(token: goodToken, label: "good"), options: RevealElementOptions()) + self.revealValueCallback.revealElements = [goodElement] + + self.revealValueCallback.onSuccess(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, goodToken) + XCTAssertEqual(goodElement.actualValue, "John") + } + func testRevealValueOnFailure() { let successToken = "123" let failureToken = "1234" @@ -179,8 +389,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -198,8 +409,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "John"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "John"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions()) @@ -211,8 +421,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -229,8 +440,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "4567890"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "4567890"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX-X", translation: ["X": "[0-9]"])) @@ -242,8 +452,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -262,8 +473,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX-XXX", translation: ["X": "[0-9]"])) @@ -275,8 +485,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -295,8 +506,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX", translation: ["X": "[0-9]"])) @@ -308,8 +518,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -329,8 +540,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["X": "[0-9]"])) @@ -342,8 +552,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -362,8 +573,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "name"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "name"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["X": "[0-9]"])) @@ -375,8 +585,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -395,8 +606,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "name"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "name"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["Y": "[0-9]"])) @@ -408,8 +618,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -436,12 +647,111 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { UIWindow().addSubview(element!) - container?.reveal(callback: callback) + container?.reveal(callback: callback.asRevealCallback) wait(for: [expectation], timeout: 20.0) - print(callback.data) - let errors = callback.data["errors"] as! [[String: NSError]] - XCTAssertTrue(errors[0]["error"]!.localizedDescription.contains("Token generated from 'getBearerToken' callback function is invalid")) + // Invalid bearer token isn't scoped to any specific token, so RevealValueCallback routes it + // through onFailure as a SkyflowError rather than folding it into the records array. + XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } - + + func testRevealValueOnFailureMixedTokenScopedAndUnscopedErrorsRoutesToOnFailure() { + // Edge case: if a genuine network/API-level error (no "token") arrives in the same batch + // as per-token errors, the whole thing is treated as a whole-request failure - the + // per-token entries are discarded rather than partially surfaced via onSuccess. This + // documents that behavior explicitly, since it's not obvious from reading either branch + // in isolation. + let scopedToken = "123" + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "network down"]) + let response: [String: Any] = [ + "errors": [ + ["token": scopedToken, "error": "Invalid Token"], + ["error": networkError] + ] + ] + + let element1 = self.container.create(input: RevealElementInput(token: scopedToken, label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["records"]) + XCTAssertEqual(self.callback.receivedResponse, "network down") + XCTAssertEqual(element1.errorMessage.text, nil) + } + + func testRevealRecordHttpCodeDefaultsToZeroWhenMissing() { + let record = RevealRecord(["token": "abc"]) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + } + + func testRevealRecordErrorDiscriminatesSuccessFromFailure() { + let success = RevealRecord(["token": "abc", "value": "1234", "httpCode": 200]) + let failure = RevealRecord(["token": "xyz", "error": "Tokens not found", "httpCode": 404]) + + XCTAssertNil(success.error) + XCTAssertEqual(failure.error, "Tokens not found") + XCTAssertEqual(failure.httpCode, 404) + } + + // The vault sends metadata's skyflowId field as "skyflowID" on the wire - RevealRecord + // normalizes it to "skyflowId" so it's consistent with CollectRecord.skyflowId, + // rather than exposing the raw wire casing to consumers. + func testRevealRecordNormalizesSkyflowIDKeyInMetadata() { + let record = RevealRecord(["token": "abc", "httpCode": 200, "metadata": ["skyflowID": "SID", "tableName": "table"]]) + let metadata = record.metadata! + XCTAssertEqual(metadata["skyflowId"] as? String, "SID") + XCTAssertNil(metadata["skyflowID"]) + XCTAssertEqual(metadata["tableName"] as? String, "table") + } + + // Exact metadata shape reported from a real device run: {"skyflowID": "426279b9-...", + // "tableName": "table5"}. + func testRevealRecordNormalizesSkyflowIDKeyInMetadataWithReportedRealWorldValues() { + let record = RevealRecord([ + "token": "abc", + "httpCode": 200, + "metadata": ["skyflowID": "426279b9-efe0-44df-bc39-16839bc86e86", "tableName": "table5"] + ]) + print("metadata:", record.metadata as Any) + let metadata = record.metadata! + XCTAssertEqual(metadata["skyflowId"] as? String, "426279b9-efe0-44df-bc39-16839bc86e86") + XCTAssertNil(metadata["skyflowID"]) + XCTAssertEqual(metadata["tableName"] as? String, "table5") + } + + func testRevealResponseInitReturnsNilForMalformedBody() { + XCTAssertNil(RevealResponse("not a dictionary")) + XCTAssertNil(RevealResponse(["typo": []])) + XCTAssertNil(RevealResponse(["records": "not an array"])) + } + + // Verifies the real end-to-end wiring for a whole-request failure through + // FlowVaultRevealAPICallback -> callRevealOnFailure -> LogCallback -> RevealCallback -> + // SkyflowError.wrap, using a token provider that fails immediately (no network mocking + // needed - TokenAPICallback's failure reaches the exact same callRevealOnFailure wrapping + // as a real dispatch failure would). This is the plumbing that had a real bug (SkyflowError.wrap + // losing the message entirely) until it was found and fixed earlier via a unit test on + // SkyflowError.wrap directly - this test instead confirms the full real call chain, not just + // the isolated function. + func testDetokenizeTokenProviderFailureSurfacesAsSkyflowErrorThroughRealCallChain() { + class FailingTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + apiCallback.onFailure(NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "TokenProvider error"])) + } + } + let client = Client(Configuration(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: FailingTokenProvider())) + let expectation = XCTestExpectation(description: "TokenProvider failure surfaces as SkyflowError") + let callback = DemoAPICallback(expectation: expectation) + + client.detokenize(records: ["records": [["token": "tok1"]]], callback: callback.asRevealCallback) + + wait(for: [expectation], timeout: 10.0) + + XCTAssertEqual(callback.receivedResponse, "TokenProvider error") + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index 6e872245..8f41579b 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -3,7 +3,8 @@ */ import XCTest -@testable import Skyflow +@testable import SkyflowFlowVaultIOS +@testable import SkyflowCore final class skyflow_iOS_utilTests: XCTestCase { override func setUp() { @@ -61,30 +62,49 @@ final class skyflow_iOS_utilTests: XCTestCase { textField.addAndFormatText(str) XCTAssertEqual(textField.secureText, "4111 1111 1111 1111") } - func testConstructRequestWithTokens() { - let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) - let result = apiClient.constructBatchRequestBody(records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: ICOptions(tokens: true)) - let record = result["records"] as! [[String: Any]] - let id = record[1]["ID"] as! String - let tokenization = record[1]["tokenization"] as! Bool - XCTAssertEqual(id, "$responses.0.records.0.skyflow_id") - XCTAssertTrue(tokenization) + func testConstructV2RequestBody() { + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions()) + XCTAssertEqual(result["vaultID"] as! String, "vault123") + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field1": "value1"]) } - - func testConstructUpdateRequestBody() { - let apiClient = APIClient(vaultID: "vault123", vaultURL: "https://example.com", tokenProvider: DemoTokenProvider()) - - let updateRecords: [String: Any] = [ - "update": [ - ["table": "users", "fields": ["name": "John Doe", "email": "john.doe@example.com"], "skyflowID": "id123"], - ["table": "users", "fields": ["name": "Jane Doe", "email": "jane.doe@example.com"], "skyflowID": "id124"] - ] + + func testConstructV2RequestBodyWithUpsert() { + let upsert = [UpsertOption(tableName: "table", uniqueColumns: ["field1"], updateType: .REPLACE)] + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let records = result["records"] as! [[String: Any]] + let upsertPayload = records[0]["upsert"] as! [String: Any] + XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) + XCTAssertEqual(upsertPayload["updateType"] as! String, "REPLACE") + } + + // Not exercised by any current call site (UpsertOption always supplies a concrete + // updateType today), but the nil default is part of the public API - confirms + // "updateType" is omitted from the wire payload rather than serialized as null. + func testConstructV2RequestBodyWithUpsertAndNilUpdateType() { + let upsert = [UpsertOption(tableName: "table", uniqueColumns: ["field1"])] + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let records = result["records"] as! [[String: Any]] + let upsertPayload = records[0]["upsert"] as! [String: Any] + XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) + XCTAssertNil(upsertPayload["updateType"]) + } + + + func testFlowVaultUpdateRequestBody() { + let records: [[String: Any]] = [ + ["skyflowID": "id1", "tableName": "table1", "data": ["email": "a@b.com"]], + ["skyflowID": "id2", "tableName": "table2", "data": ["name": "chiku"]] ] - let singleUpdateRecord = ["table": "users", "fields": ["name": "John Doe", "email": "john.doe@example.com"], "skyflowID": "id123"] as [String : Any] - let result = apiClient.constructUpdateRequestBody(records: singleUpdateRecord, options: ICOptions(tokens: false)) - let records = result["record"] as! [String: Any] - let tokenization = result["tokenization"] as! Bool - XCTAssertEqual(records as NSDictionary, ["fields": ["name": "John Doe", "email": "john.doe@example.com"]]) - XCTAssertFalse(tokenization) + + let result = FlowVaultUpdateRequestBody.createRequestBody(vaultID: "vault123", records: records) + + XCTAssertEqual(result["vaultID"] as? String, "vault123") + let resultRecords = result["records"] as! [[String: Any]] + XCTAssertEqual(resultRecords.count, 2) + XCTAssertEqual(resultRecords[0]["skyflowID"] as? String, "id1") + XCTAssertEqual(resultRecords[0]["tableName"] as? String, "table1") + XCTAssertEqual(resultRecords[1]["tableName"] as? String, "table2") } } diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 21b5ce3e..7fb57631 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -1,26 +1,39 @@ +# Both SDK products share one version line: this bumps Skyflow.podspec, +# Skyflow-flowvault-ios.podspec, and the shared SDK_VERSION together. +# +# Usage: +# ./scripts/bump_version.sh 1.27.0 # release version, tag 1.27.0 +# ./scripts/bump_version.sh 1.27.0 # dev version pinned to a commit + version=$1 SEMVER=$version +PODSPECS="Skyflow.podspec Skyflow-flowvault-ios.podspec" +VERSION_FILE="./Sources/SkyflowCore/Version.swift" + if [ -z $2 ] then - sed -E "s/spec.version .+/spec.version = \"$SEMVER\"/g" "./Skyflow.podspec" > tempfile - sed -E "s/source .+/source = { :git => \"https:\/\/github.com\/skyflowapi\/skyflow-iOS.git\", :tag => \"$1\" }/g" tempfile > ./Skyflow.podspec && rm -f tempfile - sed -E "s/var SDK_VERSION = .+/var SDK_VERSION = \"$SEMVER\"/g" ./Sources/Skyflow/Version.swift > tempfile && cat tempfile > ./Sources/Skyflow/Version.swift && rm -f tempfile + for podspec in $PODSPECS + do + sed -E "s/spec.version .+/spec.version = \"$SEMVER\"/g" "./$podspec" > tempfile + sed -E "s/source .+/source = { :git => \"https:\/\/github.com\/skyflowapi\/skyflow-iOS.git\", :tag => \"$1\" }/g" tempfile > "./$podspec" && rm -f tempfile + done + sed -E "s/var SDK_VERSION = .+/var SDK_VERSION = \"$SEMVER\"/g" $VERSION_FILE > tempfile && cat tempfile > $VERSION_FILE && rm -f tempfile echo -------------------------- - echo "Done, Pod now at v$1" + echo "Done, Pods now at v$1" else - sed -E "s/spec.version .+/spec.version = \"$SEMVER-dev.$2\"/g" "./Skyflow.podspec" > tempfile - sed -E "s/source .+/source = { :git => \"https:\/\/github.com\/skyflowapi\/skyflow-iOS.git\", :commit => \"$2\" }/g" tempfile > ./Skyflow.podspec && rm -f tempfile - sed -E "s/var SDK_VERSION = .+/var SDK_VERSION = \"$SEMVER-dev.$2\"/g" ./Sources/Skyflow/Version.swift > tempfile && cat tempfile > ./Sources/Skyflow/Version.swift && rm -f tempfile + for podspec in $PODSPECS + do + sed -E "s/spec.version .+/spec.version = \"$SEMVER-dev.$2\"/g" "./$podspec" > tempfile + sed -E "s/source .+/source = { :git => \"https:\/\/github.com\/skyflowapi\/skyflow-iOS.git\", :commit => \"$2\" }/g" tempfile > "./$podspec" && rm -f tempfile + done + sed -E "s/var SDK_VERSION = .+/var SDK_VERSION = \"$SEMVER-dev.$2\"/g" $VERSION_FILE > tempfile && cat tempfile > $VERSION_FILE && rm -f tempfile echo -------------------------- - echo "Done, Pod now at v$1-dev.$2" + echo "Done, Pods now at v$1-dev.$2" fi - - - From 35e48a11cda17c4126895320997bb9e33af986a0 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 13 Aug 2026 09:51:59 +0530 Subject: [PATCH 02/16] SK-3043 refactor code for common code separation --- Package.swift | 18 +- Samples/README.md | 4 +- Skyflow-flowvault-ios.podspec | 8 +- Sources/Skyflow/client/Client.swift | 20 ++ .../{core => client}/ClientOperations.swift | 0 .../ComposableContainerOperations.swift | 0 Sources/Skyflow/core/Client.swift | 22 -- .../CollectElementInput.swift | 0 .../CollectElementOptions.swift | 0 .../{collect => models}/CollectOptions.swift | 0 .../{core => models}/Configuration.swift | 0 .../{core => models}/ContainerOptions.swift | 0 .../{reveal => models}/GetByIdRecord.swift | 0 .../{reveal => models}/GetOptions.swift | 0 .../{reveal => models}/GetRecord.swift | 0 .../{collect => models}/InsertOptions.swift | 0 .../RevealElementInput.swift | 0 .../RevealElementOptions.swift | 0 .../{reveal => models}/RevealOptions.swift | 0 .../APIClientLegacy.swift | 0 .../CollectAPICallback.swift | 0 .../GetAPICallback.swift | 0 .../InsertAPICallback.swift | 0 .../RevealApiCallback.swift | 0 .../RevealByIDAPICallback.swift | 0 .../RevealRequestRecord.swift | 0 .../validations/RequestValidators.swift | 232 ++++++++++++++ .../{core => client}/ClientBase.swift | 0 .../SkyflowCore/{core => client}/Env.swift | 0 .../{core => client}/Options.swift | 0 .../{ => collect/elements}/BaseElement.swift | 0 .../{core => collect/elements}/Element.swift | 0 .../elements}/ElementType.swift | 0 .../elements/FormatTextField.swift | 0 .../{ => collect}/elements/PaddingLabel.swift | 0 .../elements/SkyflowElement.swift | 0 .../{ => collect}/elements/TextField.swift | 0 .../TextFieldValidationDelegate.swift | 0 .../elements}/state/State.swift | 0 .../elements}/state/StateforText.swift | 0 .../elements}/styles/Style.swift | 0 .../elements}/styles/Styles.swift | 0 .../BaseContainerOptions.swift | 0 .../container => containers}/Container.swift | 0 .../ContainerProtocol.swift | 0 .../ContainerType.swift | 0 Sources/SkyflowCore/errors/ErrorCodes.swift | 2 +- .../{core => logging}/ContextOptions.swift | 0 .../{core => logging}/EventName.swift | 0 .../{core => logging}/InterfaceName.swift | 0 .../SkyflowCore/{core => logging}/Log.swift | 0 .../{core => logging}/LogLevel.swift | 0 .../{core => logging}/Message.swift | 0 .../AutoshiftAllowed.swift | 0 .../BaseCollectElementInput.swift | 0 .../BaseCollectElementOptions.swift | 0 .../{core => models}/BaseConfiguration.swift | 0 .../BaseRevealElementInput.swift | 0 .../BaseRevealElementOptions.swift | 0 .../{reveal => models}/RedactionType.swift | 0 .../{core => networking}/APIClient.swift | 0 .../TokenAPICallback.swift | 0 .../{core => }/protocols/Callback.swift | 0 .../{core => }/protocols/TokenProvider.swift | 0 .../{elements => }/utils/CardBin.swift | 0 .../{elements => }/utils/CardType.swift | 0 .../{elements => }/utils/CharacterSet.swift | 0 .../{reveal => utils}/ConversionHelpers.swift | 0 .../validations/ElementValueMatchRule.swift | 0 .../validations/LengthMatchRule.swift | 0 .../validations/RegexMatchRule.swift | 0 .../SkyflowInternalValidatorProtocol.swift | 0 .../SkyflowValidateCardExpirationDate.swift | 0 .../SkyflowValidateCardNumber.swift | 0 .../SkyflowValidateExpirationMonth.swift | 0 .../SkyflowValidateExpirationYear.swift | 0 .../SkyflowValidateLengthMatch.swift | 0 .../validations/SkyflowValidationError.swift | 0 .../validations/SkyflowValidator.swift | 0 .../validations/ValidationRule.swift | 0 .../validations/ValidationSet.swift | 0 .../Exports.swift | 2 +- .../Init.swift | 2 +- Sources/SkyflowFlowVault/client/Client.swift | 20 ++ .../client}/ClientOperations.swift | 36 +-- .../collect/CollectContainerOperations.swift | 46 +-- .../collect/CollectRequestBuilder.swift} | 41 ++- .../ComposableContainerOperations.swift | 47 +-- .../composable}/ContainerOptions.swift | 0 .../models}/AdditionalFields.swift | 0 .../models}/CollectElementInput.swift | 0 .../models}/CollectElementOptions.swift | 0 .../models}/CollectOptions.swift | 0 .../models}/CollectResponse.swift | 0 .../models}/Configuration.swift | 0 .../models}/InsertOptions.swift | 0 .../models}/RevealElementInput.swift | 0 .../models}/RevealElementOptions.swift | 0 .../models}/RevealOptions.swift | 0 .../models}/RevealRequestRecord.swift | 0 .../models}/RevealResponse.swift | 0 .../models}/TokenGroupRedaction.swift | 0 .../models}/UpsertOption.swift | 0 .../networking}/APIClientFlowVault.swift | 8 +- .../FlowVaultCollectAPICallback.swift | 19 +- .../FlowVaultRevealApiCallback.swift | 2 +- .../reveal/RevealContainerOperations.swift | 37 +-- .../reveal/RevealRequestBuilder.swift | 35 +++ .../reveal/RevealValueCallback.swift | 0 .../utils}/CVVTokenReplacer.swift | 24 +- .../validations/RequestValidators.swift | 95 ++++++ .../collect/CVVMockGenerator.swift | 30 -- .../collect/FlowVaultICOptions.swift | 46 --- .../collect/FlowVaultInsertAPICallback.swift | 295 ------------------ .../collect/FlowVaultInsertRequestBody.swift | 29 -- .../collect/FlowVaultUpdateRequestBody.swift | 13 - Sources/SkyflowFlowVaultIOS/core/Client.swift | 22 -- .../FlowVaultDetokenizeRequestBody.swift | 20 -- .../reveal/RevealRequestBody.swift | 20 -- Tests/skyflow-iOS-collectTests/DemoImpl.swift | 2 +- .../skyflow_iOS_collectTests.swift | 6 +- ...ow_iOS_emptySkyflowIdValidationTests.swift | 14 +- .../DemoImpl.swift | 2 +- ...skyflow_iOS_composableEelementsTests.swift | 4 +- .../CollectElementOptionsTests.swift | 2 +- Tests/skyflow-iOS-elementTests/DemoImpl.swift | 2 +- .../InputFormattingTests.swift | 4 +- .../TextFieldDelegateTests.swift | 4 +- .../skyflow_iOS_containerTests.swift | 2 +- .../skyflow_iOS_elementTests.swift | 2 +- Tests/skyflow-iOS-errorTests/DemoImpl.swift | 2 +- .../skyflow-iOS-errorTests/GatewayImpl.swift | 2 +- .../Skyflow_iOS_collectErrorTests.swift | 44 +-- .../Skyflow_iOS_generalErrorTests.swift | 2 +- .../Skyflow_iOS_revealErrorTests.swift | 2 +- ...low_iOS_revealOptionsValidationTests.swift | 2 +- .../MockURLProtocol.swift | 2 +- ...w_iOS_dormantCollectRequestBodyTests.swift | 2 +- .../skyflow_iOS_legacyUtilTests.swift | 2 +- Tests/skyflow-iOS-revealTests/DemoImpl.swift | 2 +- .../RevealElementOptionsTests.swift | 2 +- .../RevealSkyflowLabelViewTests.swift | 4 +- .../skyflow_iOS_revealTests.swift | 4 +- Tests/skyflow-iOS-utilTests/DemoImpl.swift | 2 +- .../MockURLProtocol.swift | 2 +- .../skyflow_iOS_collectUtilTests.swift | 42 +-- .../skyflow_iOS_cvvMockTests.swift | 6 +- .../skyflow_iOS_insertUtilTests 2.swift | 20 +- .../skyflow_iOS_revealUtilTests.swift | 8 +- .../skyflow_iOS_utilTests.swift | 12 +- 150 files changed, 631 insertions(+), 772 deletions(-) create mode 100644 Sources/Skyflow/client/Client.swift rename Sources/Skyflow/{core => client}/ClientOperations.swift (100%) rename Sources/Skyflow/{collect => composable}/ComposableContainerOperations.swift (100%) delete mode 100644 Sources/Skyflow/core/Client.swift rename Sources/Skyflow/{elements => models}/CollectElementInput.swift (100%) rename Sources/Skyflow/{elements => models}/CollectElementOptions.swift (100%) rename Sources/Skyflow/{collect => models}/CollectOptions.swift (100%) rename Sources/Skyflow/{core => models}/Configuration.swift (100%) rename Sources/Skyflow/{core => models}/ContainerOptions.swift (100%) rename Sources/Skyflow/{reveal => models}/GetByIdRecord.swift (100%) rename Sources/Skyflow/{reveal => models}/GetOptions.swift (100%) rename Sources/Skyflow/{reveal => models}/GetRecord.swift (100%) rename Sources/Skyflow/{collect => models}/InsertOptions.swift (100%) rename Sources/Skyflow/{reveal => models}/RevealElementInput.swift (100%) rename Sources/Skyflow/{reveal => models}/RevealElementOptions.swift (100%) rename Sources/Skyflow/{reveal => models}/RevealOptions.swift (100%) rename Sources/Skyflow/{core => networking}/APIClientLegacy.swift (100%) rename Sources/Skyflow/{collect => networking}/CollectAPICallback.swift (100%) rename Sources/Skyflow/{reveal => networking}/GetAPICallback.swift (100%) rename Sources/Skyflow/{collect => networking}/InsertAPICallback.swift (100%) rename Sources/Skyflow/{reveal => networking}/RevealApiCallback.swift (100%) rename Sources/Skyflow/{reveal => networking}/RevealByIDAPICallback.swift (100%) rename Sources/Skyflow/{reveal => networking}/RevealRequestRecord.swift (100%) create mode 100644 Sources/Skyflow/validations/RequestValidators.swift rename Sources/SkyflowCore/{core => client}/ClientBase.swift (100%) rename Sources/SkyflowCore/{core => client}/Env.swift (100%) rename Sources/SkyflowCore/{core => client}/Options.swift (100%) rename Sources/SkyflowCore/{ => collect/elements}/BaseElement.swift (100%) rename Sources/SkyflowCore/{core => collect/elements}/Element.swift (100%) rename Sources/SkyflowCore/{elements/core => collect/elements}/ElementType.swift (100%) rename Sources/SkyflowCore/{ => collect}/elements/FormatTextField.swift (100%) rename Sources/SkyflowCore/{ => collect}/elements/PaddingLabel.swift (100%) rename Sources/SkyflowCore/{ => collect}/elements/SkyflowElement.swift (100%) rename Sources/SkyflowCore/{ => collect}/elements/TextField.swift (100%) rename Sources/SkyflowCore/{ => collect}/elements/TextFieldValidationDelegate.swift (100%) rename Sources/SkyflowCore/{elements/core => collect/elements}/state/State.swift (100%) rename Sources/SkyflowCore/{elements/core => collect/elements}/state/StateforText.swift (100%) rename Sources/SkyflowCore/{elements/core => collect/elements}/styles/Style.swift (100%) rename Sources/SkyflowCore/{elements/core => collect/elements}/styles/Styles.swift (100%) rename Sources/SkyflowCore/{core/container => composable}/BaseContainerOptions.swift (100%) rename Sources/SkyflowCore/{core/container => containers}/Container.swift (100%) rename Sources/SkyflowCore/{core/container => containers}/ContainerProtocol.swift (100%) rename Sources/SkyflowCore/{core/container => containers}/ContainerType.swift (100%) rename Sources/SkyflowCore/{core => logging}/ContextOptions.swift (100%) rename Sources/SkyflowCore/{core => logging}/EventName.swift (100%) rename Sources/SkyflowCore/{core => logging}/InterfaceName.swift (100%) rename Sources/SkyflowCore/{core => logging}/Log.swift (100%) rename Sources/SkyflowCore/{core => logging}/LogLevel.swift (100%) rename Sources/SkyflowCore/{core => logging}/Message.swift (100%) rename Sources/SkyflowCore/{composable => models}/AutoshiftAllowed.swift (100%) rename Sources/SkyflowCore/{elements/core => models}/BaseCollectElementInput.swift (100%) rename Sources/SkyflowCore/{elements/core => models}/BaseCollectElementOptions.swift (100%) rename Sources/SkyflowCore/{core => models}/BaseConfiguration.swift (100%) rename Sources/SkyflowCore/{reveal => models}/BaseRevealElementInput.swift (100%) rename Sources/SkyflowCore/{reveal => models}/BaseRevealElementOptions.swift (100%) rename Sources/SkyflowCore/{reveal => models}/RedactionType.swift (100%) rename Sources/SkyflowCore/{core => networking}/APIClient.swift (100%) rename Sources/SkyflowCore/{collect => networking}/TokenAPICallback.swift (100%) rename Sources/SkyflowCore/{core => }/protocols/Callback.swift (100%) rename Sources/SkyflowCore/{core => }/protocols/TokenProvider.swift (100%) rename Sources/SkyflowCore/{elements => }/utils/CardBin.swift (100%) rename Sources/SkyflowCore/{elements => }/utils/CardType.swift (100%) rename Sources/SkyflowCore/{elements => }/utils/CharacterSet.swift (100%) rename Sources/SkyflowCore/{reveal => utils}/ConversionHelpers.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/ElementValueMatchRule.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/LengthMatchRule.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/RegexMatchRule.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowInternalValidatorProtocol.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidateCardExpirationDate.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidateCardNumber.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidateExpirationMonth.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidateExpirationYear.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidateLengthMatch.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidationError.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/SkyflowValidator.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/ValidationRule.swift (100%) rename Sources/SkyflowCore/{elements => }/validations/ValidationSet.swift (100%) rename Sources/{SkyflowFlowVaultIOS => SkyflowFlowVault}/Exports.swift (85%) rename Sources/{SkyflowFlowVaultIOS => SkyflowFlowVault}/Init.swift (75%) create mode 100644 Sources/SkyflowFlowVault/client/Client.swift rename Sources/{SkyflowFlowVaultIOS/core => SkyflowFlowVault/client}/ClientOperations.swift (78%) rename Sources/{SkyflowFlowVaultIOS => SkyflowFlowVault}/collect/CollectContainerOperations.swift (63%) rename Sources/{SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift => SkyflowFlowVault/collect/CollectRequestBuilder.swift} (83%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/composable}/ComposableContainerOperations.swift (63%) rename Sources/{SkyflowFlowVaultIOS/core => SkyflowFlowVault/composable}/ContainerOptions.swift (100%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/models}/AdditionalFields.swift (100%) rename Sources/{SkyflowFlowVaultIOS/elements => SkyflowFlowVault/models}/CollectElementInput.swift (100%) rename Sources/{SkyflowFlowVaultIOS/elements => SkyflowFlowVault/models}/CollectElementOptions.swift (100%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/models}/CollectOptions.swift (100%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/models}/CollectResponse.swift (100%) rename Sources/{SkyflowFlowVaultIOS/core => SkyflowFlowVault/models}/Configuration.swift (100%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/models}/InsertOptions.swift (100%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/models}/RevealElementInput.swift (100%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/models}/RevealElementOptions.swift (100%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/models}/RevealOptions.swift (100%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/models}/RevealRequestRecord.swift (100%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/models}/RevealResponse.swift (100%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/models}/TokenGroupRedaction.swift (100%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/models}/UpsertOption.swift (100%) rename Sources/{SkyflowFlowVaultIOS/core => SkyflowFlowVault/networking}/APIClientFlowVault.swift (71%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/networking}/FlowVaultCollectAPICallback.swift (93%) rename Sources/{SkyflowFlowVaultIOS/reveal => SkyflowFlowVault/networking}/FlowVaultRevealApiCallback.swift (99%) rename Sources/{SkyflowFlowVaultIOS => SkyflowFlowVault}/reveal/RevealContainerOperations.swift (57%) create mode 100644 Sources/SkyflowFlowVault/reveal/RevealRequestBuilder.swift rename Sources/{SkyflowFlowVaultIOS => SkyflowFlowVault}/reveal/RevealValueCallback.swift (100%) rename Sources/{SkyflowFlowVaultIOS/collect => SkyflowFlowVault/utils}/CVVTokenReplacer.swift (86%) create mode 100644 Sources/SkyflowFlowVault/validations/RequestValidators.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultICOptions.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/core/Client.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift delete mode 100644 Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift diff --git a/Package.swift b/Package.swift index 8dbbe82b..d2f09caa 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,10 @@ let package = Package( .library( name: "Skyflow", targets: ["Skyflow"]), - // FlowVault SDK (v2 API contract). Module name is SkyflowFlowVaultIOS. + // FlowVault SDK (v2 API contract). Module name is SkyflowFlowVault. .library( name: "Skyflow-flowvault-ios", - targets: ["SkyflowFlowVaultIOS"]), + targets: ["SkyflowFlowVault"]), // check this ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -39,28 +39,28 @@ let package = Package( ), // FlowVault (v2) contract layer. .target( - name: "SkyflowFlowVaultIOS", + name: "SkyflowFlowVault", dependencies: ["SkyflowCore"] ), .testTarget( name: "skyflow-iOS-collectTests", - dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-revealTests", - dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-errorTests", - dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-getByIdTests", dependencies: ["Skyflow", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-elementTests", - dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-utilTests", - dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-legacyTests", dependencies: ["Skyflow", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-scenarioTests", dependencies: ["Skyflow", "SkyflowCore"]), .testTarget(name: "skyflow-iOS-getTests", dependencies: ["Skyflow", "SkyflowCore"]), - .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["SkyflowFlowVaultIOS", "SkyflowCore"]) + .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"]) ] ) diff --git a/Samples/README.md b/Samples/README.md index 2b4266ec..933044f2 100644 --- a/Samples/README.md +++ b/Samples/README.md @@ -12,11 +12,11 @@ Sample apps are organized by SDK — pick the folder for the product you use. | [UpsertFeature](Skyflow/UpsertFeature/) | Insert with upsert options | | [Validations](Skyflow/Validations/) | Built-in and custom validation rules | -## [`SkyflowFlowVaultIOS/`](SkyflowFlowVaultIOS/) — Skyflow FlowVault SDK (`pod 'Skyflow-flowvault-ios'` / `import SkyflowFlowVaultIOS`) +## [`SkyflowFlowVault/`](SkyflowFlowVault/) — Skyflow FlowVault SDK (`pod 'Skyflow-flowvault-ios'` / `import SkyflowFlowVault`) | Sample | Demonstrates | |---|---| -| [UpdateDataUsingElements](SkyflowFlowVaultIOS/UpdateDataUsingElements/) | Updating vault records via collect elements with `skyflowId`; its Podfile also shows installing **both** SDKs side by side in one app | +| [UpdateDataUsingElements](SkyflowFlowVault/UpdateDataUsingElements/) | Updating vault records via collect elements with `skyflowId`; its Podfile also shows installing **both** SDKs side by side in one app | ## Running a sample diff --git a/Skyflow-flowvault-ios.podspec b/Skyflow-flowvault-ios.podspec index f12efd9c..3d93d96a 100644 --- a/Skyflow-flowvault-ios.podspec +++ b/Skyflow-flowvault-ios.podspec @@ -2,9 +2,9 @@ Pod::Spec.new do |spec| spec.name = "Skyflow-flowvault-ios" - # Consumers import the SDK as `import SkyflowFlowVaultIOS` (Swift module names + # Consumers import the SDK as `import SkyflowFlowVault` (Swift module names # cannot contain hyphens). - spec.module_name = "SkyflowFlowVaultIOS" + spec.module_name = "SkyflowFlowVault" spec.version = "1.26.0-beta.1" @@ -29,9 +29,9 @@ Pod::Spec.new do |spec| # CocoaPods compiles the shared core and the FlowVault contract layer into a # single module, so the SPM-only re-export shim is excluded. - spec.source_files = "Sources/SkyflowCore/**/*.{swift}", "Sources/SkyflowFlowVaultIOS/**/*.{swift}" + spec.source_files = "Sources/SkyflowCore/**/*.{swift}", "Sources/SkyflowFlowVault/**/*.{swift}" - spec.exclude_files = "Sources/SkyflowFlowVaultIOS/Exports.swift" + spec.exclude_files = "Sources/SkyflowFlowVault/Exports.swift" # `package` access-level declarations (shared across the SPM targets) require a # package name when compiled outside SPM. diff --git a/Sources/Skyflow/client/Client.swift b/Sources/Skyflow/client/Client.swift new file mode 100644 index 00000000..f9bf19c6 --- /dev/null +++ b/Sources/Skyflow/client/Client.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Skyflow (legacy v1) entry points on the shared SkyflowCore Client class. +// The v1 operations live in ClientOperations.swift as further extensions. +// Future enhancements specific to this SDK go in extensions in this target +// without affecting the FlowVault SDK. + +import Foundation + +public extension Client { + convenience init(_ skyflowConfig: Configuration) { + self.init(skyflowConfig.data) + } + + func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { + return makeContainer(type: type, options: options?.data) + } +} diff --git a/Sources/Skyflow/core/ClientOperations.swift b/Sources/Skyflow/client/ClientOperations.swift similarity index 100% rename from Sources/Skyflow/core/ClientOperations.swift rename to Sources/Skyflow/client/ClientOperations.swift diff --git a/Sources/Skyflow/collect/ComposableContainerOperations.swift b/Sources/Skyflow/composable/ComposableContainerOperations.swift similarity index 100% rename from Sources/Skyflow/collect/ComposableContainerOperations.swift rename to Sources/Skyflow/composable/ComposableContainerOperations.swift diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift deleted file mode 100644 index 7d318dec..00000000 --- a/Sources/Skyflow/core/Client.swift +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Implementation of the Skyflow Client class (legacy v1 contract). -// The v1 operations live in ClientOperations.swift as an extension of this -// class. Future enhancements specific to this SDK go here (or in extensions -// in this target) without affecting the FlowVault SDK. - -import Foundation - -public class Client: ClientBase { - public init(_ skyflowConfig: Configuration) { - super.init(skyflowConfig.data) - } -} - -public extension Client { - func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { - return makeContainer(type: type, options: options?.data) - } -} diff --git a/Sources/Skyflow/elements/CollectElementInput.swift b/Sources/Skyflow/models/CollectElementInput.swift similarity index 100% rename from Sources/Skyflow/elements/CollectElementInput.swift rename to Sources/Skyflow/models/CollectElementInput.swift diff --git a/Sources/Skyflow/elements/CollectElementOptions.swift b/Sources/Skyflow/models/CollectElementOptions.swift similarity index 100% rename from Sources/Skyflow/elements/CollectElementOptions.swift rename to Sources/Skyflow/models/CollectElementOptions.swift diff --git a/Sources/Skyflow/collect/CollectOptions.swift b/Sources/Skyflow/models/CollectOptions.swift similarity index 100% rename from Sources/Skyflow/collect/CollectOptions.swift rename to Sources/Skyflow/models/CollectOptions.swift diff --git a/Sources/Skyflow/core/Configuration.swift b/Sources/Skyflow/models/Configuration.swift similarity index 100% rename from Sources/Skyflow/core/Configuration.swift rename to Sources/Skyflow/models/Configuration.swift diff --git a/Sources/Skyflow/core/ContainerOptions.swift b/Sources/Skyflow/models/ContainerOptions.swift similarity index 100% rename from Sources/Skyflow/core/ContainerOptions.swift rename to Sources/Skyflow/models/ContainerOptions.swift diff --git a/Sources/Skyflow/reveal/GetByIdRecord.swift b/Sources/Skyflow/models/GetByIdRecord.swift similarity index 100% rename from Sources/Skyflow/reveal/GetByIdRecord.swift rename to Sources/Skyflow/models/GetByIdRecord.swift diff --git a/Sources/Skyflow/reveal/GetOptions.swift b/Sources/Skyflow/models/GetOptions.swift similarity index 100% rename from Sources/Skyflow/reveal/GetOptions.swift rename to Sources/Skyflow/models/GetOptions.swift diff --git a/Sources/Skyflow/reveal/GetRecord.swift b/Sources/Skyflow/models/GetRecord.swift similarity index 100% rename from Sources/Skyflow/reveal/GetRecord.swift rename to Sources/Skyflow/models/GetRecord.swift diff --git a/Sources/Skyflow/collect/InsertOptions.swift b/Sources/Skyflow/models/InsertOptions.swift similarity index 100% rename from Sources/Skyflow/collect/InsertOptions.swift rename to Sources/Skyflow/models/InsertOptions.swift diff --git a/Sources/Skyflow/reveal/RevealElementInput.swift b/Sources/Skyflow/models/RevealElementInput.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealElementInput.swift rename to Sources/Skyflow/models/RevealElementInput.swift diff --git a/Sources/Skyflow/reveal/RevealElementOptions.swift b/Sources/Skyflow/models/RevealElementOptions.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealElementOptions.swift rename to Sources/Skyflow/models/RevealElementOptions.swift diff --git a/Sources/Skyflow/reveal/RevealOptions.swift b/Sources/Skyflow/models/RevealOptions.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealOptions.swift rename to Sources/Skyflow/models/RevealOptions.swift diff --git a/Sources/Skyflow/core/APIClientLegacy.swift b/Sources/Skyflow/networking/APIClientLegacy.swift similarity index 100% rename from Sources/Skyflow/core/APIClientLegacy.swift rename to Sources/Skyflow/networking/APIClientLegacy.swift diff --git a/Sources/Skyflow/collect/CollectAPICallback.swift b/Sources/Skyflow/networking/CollectAPICallback.swift similarity index 100% rename from Sources/Skyflow/collect/CollectAPICallback.swift rename to Sources/Skyflow/networking/CollectAPICallback.swift diff --git a/Sources/Skyflow/reveal/GetAPICallback.swift b/Sources/Skyflow/networking/GetAPICallback.swift similarity index 100% rename from Sources/Skyflow/reveal/GetAPICallback.swift rename to Sources/Skyflow/networking/GetAPICallback.swift diff --git a/Sources/Skyflow/collect/InsertAPICallback.swift b/Sources/Skyflow/networking/InsertAPICallback.swift similarity index 100% rename from Sources/Skyflow/collect/InsertAPICallback.swift rename to Sources/Skyflow/networking/InsertAPICallback.swift diff --git a/Sources/Skyflow/reveal/RevealApiCallback.swift b/Sources/Skyflow/networking/RevealApiCallback.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealApiCallback.swift rename to Sources/Skyflow/networking/RevealApiCallback.swift diff --git a/Sources/Skyflow/reveal/RevealByIDAPICallback.swift b/Sources/Skyflow/networking/RevealByIDAPICallback.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealByIDAPICallback.swift rename to Sources/Skyflow/networking/RevealByIDAPICallback.swift diff --git a/Sources/Skyflow/reveal/RevealRequestRecord.swift b/Sources/Skyflow/networking/RevealRequestRecord.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealRequestRecord.swift rename to Sources/Skyflow/networking/RevealRequestRecord.swift diff --git a/Sources/Skyflow/validations/RequestValidators.swift b/Sources/Skyflow/validations/RequestValidators.swift new file mode 100644 index 00000000..0c364577 --- /dev/null +++ b/Sources/Skyflow/validations/RequestValidators.swift @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Pre-flight validators for the legacy (v1) request flows, shared by the +// container operations and the client-level operations. Stateless: only +// static functions, never instantiated. Validators compute and return the +// error; the call sites own delivering the failure to their callback. + +import Foundation + +internal class RequestValidators { + // Guards shared by every operation: the client must have been configured + // with a vault ID and URL. (An empty vaultURL becomes "/" in SkyflowCore.Client.) + internal static func checkClientConfig(vaultID: String, vaultURL: String) -> ErrorCodes? { + if vaultID.isEmpty { + return .EMPTY_VAULT_ID() + } + if vaultURL == "/" { + return .EMPTY_VAULT_URL() + } + return nil + } + + internal static func checkElement(element: TextField) -> ErrorCodes? { + if element.collectInput.tableName.isEmpty { + return .EMPTY_TABLE_NAME_IN_COLLECT() + } + if element.collectInput.column.isEmpty { + return .EMPTY_COLUMN_NAME_IN_COLLECT() + } + if !element.isMounted() { + return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + } + + return nil + } + + internal static func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { + if record["table"] == nil { + return .TABLE_KEY_ERROR(value: "\(index)") + } + if !(record["table"] is String) { + return .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } + if (record["table"] as? String == "") { + return .EMPTY_TABLE_NAME() + } + if record["fields"] == nil { + return .FIELDS_KEY_ERROR(value: "\(index)") + } + if !(record["fields"] is [String: Any]) { + return .INVALID_FIELDS_TYPE(value: "\(index)") + } + let fields = record["fields"] as! [String: Any] + if (fields.isEmpty){ + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + + return nil + } + + internal static func checkRevealElements(elements: [Label]) -> ErrorCodes? { + for element in elements { + if element.errorTriggered { + return .ERROR_TRIGGERED(value: element.triggeredErrorMessage) + } + if element.getToken().isEmpty { + return .EMPTY_TOKEN_ID() + } + } + return nil + } + + internal static func checkDetokenizeRecord(token: [String: Any], index: Int) -> ErrorCodes? { + if token["redaction"] != nil { + guard let _ = token["redaction"] as? RedactionType else { + return .INVALID_REDACTION_TYPE() + } + } + if token["token"] == nil { + return .ID_KEY_ERROR() + } else { + guard let _ = token["token"] as? String else { + return .INVALID_TOKEN_TYPE(value: "\(index)") + } + } + return nil + } + + internal static func checkGetByIdEntry(entry: [String: Any], index: Int) -> ErrorCodes? { + if entry.isEmpty { + return .EMPTY_RECORDS_OBJECT() + } + if entry["ids"] == nil { + return .MISSING_KEY_IDS(value: "\(index)") + } + if !(entry["ids"] is [String]) { + return .INVALID_IDS_TYPE() + } + if ((entry["ids"] as? [String])?.count == 0) { + return .EMPTY_IDS(value: "\(index)") + } + let ids = entry["ids"] as! [String] + for id in ids { + if (id == "") { + return .EMPTY_ID_VALUE(value: "\(index)") + } + } + if entry["table"] == nil { + return .TABLE_KEY_ERROR(value: "\(index)") + } + if !(entry["table"] is String) { + return .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } + if ((entry["table"] as? String) == "") { + return .EMPTY_TABLE_NAME() + } + if entry["redaction"] == nil { + return .REDACTION_KEY_ERROR(value: "\(index)") + } + if (entry["redaction"] as? RedactionType) != nil { + return nil + } else { + return .INVALID_REDACTION_TYPE() + } + } + + internal static func checkUpsertOptions(_ upsert: [[String: Any]]) -> ErrorCodes? { + if upsert.count == 0 { + return .UPSERT_OPTION_CANNOT_BE_EMPTY() + } + for (index, currUpsertOption) in upsert.enumerated() { + if currUpsertOption["table"] == nil { + return .MISSING_TABLE_NAME_IN_USERT_OPTION(value: "\(index)") + } + if currUpsertOption["column"] == nil { + return .MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "\(index)") + } + if currUpsertOption["table"] as! String == "" { + return .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + } + if currUpsertOption["column"] as! String == "" { + return .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + } + } + return nil + } + + // Validation for the v1 get records dictionaries (moved from Client: + // only the legacy contract exposes the get operation). + internal static func validateGetRecords(entry: [String: Any], getOptions: GetOptions, index: Int) -> ErrorCodes? { + if entry.isEmpty { + return .EMPTY_RECORDS_OBJECT() + } + if (entry["ids"] != nil ){ + if !(entry["ids"] is [String]) { + return .INVALID_IDS_TYPE() + } + if ((entry["ids"] as? [String])?.count == 0) { + return .EMPTY_IDS(value: "\(index)") + } + let ids = entry["ids"] as! [String] + for id in ids { + if (id == "") { + return .EMPTY_ID_VALUE(value: "\(index)") + } + } + } + if entry["table"] == nil { + return .TABLE_KEY_ERROR(value: "\(index)") + } + if !(entry["table"] is String) { + return .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } + if ((entry["table"] as? String) == "") { + return .EMPTY_TABLE_NAME() + } + + if( getOptions.tokens == true ){ + if (entry["columnName"] != nil || entry["columnValues"] != nil){ + return .TOKENS_GET_COLUMN_NOT_SUPPPORTED() + + } + if (entry["redaction"] as? RedactionType) != nil { + return .REDACTION_WITH_TOKEN_NOT_SUPPORTED() + } + } else { + if entry["redaction"] == nil { + return .REDACTION_KEY_ERROR(value: "\(index)") + } else if (entry["redaction"] as? RedactionType) == nil { + return .INVALID_REDACTION_TYPE() + } + } + + if(entry["columnName"] == nil){ + if ((entry["ids"] == nil) && (entry["columnValues"] == nil)){ + return .MISSING_IDS_OR_COLUMN_VALUES_IN_GET() + } + } else if (entry["columnName"] != nil && entry["columnValues"] == nil){ + return .MISSING_RECORD_COLUMN_VALUE() + } else if !(entry["columnName"] is String){ + return .INVALID_COLUMN_NAME(value: "\(index)") + } else if ((entry["ids"] != nil) && (entry["columnName"] != nil)){ + return .SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED() + } + if (entry["columnValues"] != nil){ + if ((entry["columnValues"] as? [String])?.count == 0) { + return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") + } + if !(entry["columnValues"] is [String]) { + return .INVALID_COLUMN_VALUES_IN_GET() + } + if ((entry["columnValues"] as? [String])?.count == 0) { + return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") + } + let columnValues = entry["columnValues"] as! [String] + for columnValue in columnValues { + if (columnValue == "") { + return .EMPTY_COLUMN_VALUE(value: "\(index)") + } + } + if( entry["columnName"] == nil ){ + return .MISSING_COLUMN_NAME() + } else if((entry["columnName"] as? String) == ""){ + return .EMPTY_COLUMN_NAME() + } + } + + return nil + } +} diff --git a/Sources/SkyflowCore/core/ClientBase.swift b/Sources/SkyflowCore/client/ClientBase.swift similarity index 100% rename from Sources/SkyflowCore/core/ClientBase.swift rename to Sources/SkyflowCore/client/ClientBase.swift diff --git a/Sources/SkyflowCore/core/Env.swift b/Sources/SkyflowCore/client/Env.swift similarity index 100% rename from Sources/SkyflowCore/core/Env.swift rename to Sources/SkyflowCore/client/Env.swift diff --git a/Sources/SkyflowCore/core/Options.swift b/Sources/SkyflowCore/client/Options.swift similarity index 100% rename from Sources/SkyflowCore/core/Options.swift rename to Sources/SkyflowCore/client/Options.swift diff --git a/Sources/SkyflowCore/BaseElement.swift b/Sources/SkyflowCore/collect/elements/BaseElement.swift similarity index 100% rename from Sources/SkyflowCore/BaseElement.swift rename to Sources/SkyflowCore/collect/elements/BaseElement.swift diff --git a/Sources/SkyflowCore/core/Element.swift b/Sources/SkyflowCore/collect/elements/Element.swift similarity index 100% rename from Sources/SkyflowCore/core/Element.swift rename to Sources/SkyflowCore/collect/elements/Element.swift diff --git a/Sources/SkyflowCore/elements/core/ElementType.swift b/Sources/SkyflowCore/collect/elements/ElementType.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/ElementType.swift rename to Sources/SkyflowCore/collect/elements/ElementType.swift diff --git a/Sources/SkyflowCore/elements/FormatTextField.swift b/Sources/SkyflowCore/collect/elements/FormatTextField.swift similarity index 100% rename from Sources/SkyflowCore/elements/FormatTextField.swift rename to Sources/SkyflowCore/collect/elements/FormatTextField.swift diff --git a/Sources/SkyflowCore/elements/PaddingLabel.swift b/Sources/SkyflowCore/collect/elements/PaddingLabel.swift similarity index 100% rename from Sources/SkyflowCore/elements/PaddingLabel.swift rename to Sources/SkyflowCore/collect/elements/PaddingLabel.swift diff --git a/Sources/SkyflowCore/elements/SkyflowElement.swift b/Sources/SkyflowCore/collect/elements/SkyflowElement.swift similarity index 100% rename from Sources/SkyflowCore/elements/SkyflowElement.swift rename to Sources/SkyflowCore/collect/elements/SkyflowElement.swift diff --git a/Sources/SkyflowCore/elements/TextField.swift b/Sources/SkyflowCore/collect/elements/TextField.swift similarity index 100% rename from Sources/SkyflowCore/elements/TextField.swift rename to Sources/SkyflowCore/collect/elements/TextField.swift diff --git a/Sources/SkyflowCore/elements/TextFieldValidationDelegate.swift b/Sources/SkyflowCore/collect/elements/TextFieldValidationDelegate.swift similarity index 100% rename from Sources/SkyflowCore/elements/TextFieldValidationDelegate.swift rename to Sources/SkyflowCore/collect/elements/TextFieldValidationDelegate.swift diff --git a/Sources/SkyflowCore/elements/core/state/State.swift b/Sources/SkyflowCore/collect/elements/state/State.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/state/State.swift rename to Sources/SkyflowCore/collect/elements/state/State.swift diff --git a/Sources/SkyflowCore/elements/core/state/StateforText.swift b/Sources/SkyflowCore/collect/elements/state/StateforText.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/state/StateforText.swift rename to Sources/SkyflowCore/collect/elements/state/StateforText.swift diff --git a/Sources/SkyflowCore/elements/core/styles/Style.swift b/Sources/SkyflowCore/collect/elements/styles/Style.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/styles/Style.swift rename to Sources/SkyflowCore/collect/elements/styles/Style.swift diff --git a/Sources/SkyflowCore/elements/core/styles/Styles.swift b/Sources/SkyflowCore/collect/elements/styles/Styles.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/styles/Styles.swift rename to Sources/SkyflowCore/collect/elements/styles/Styles.swift diff --git a/Sources/SkyflowCore/core/container/BaseContainerOptions.swift b/Sources/SkyflowCore/composable/BaseContainerOptions.swift similarity index 100% rename from Sources/SkyflowCore/core/container/BaseContainerOptions.swift rename to Sources/SkyflowCore/composable/BaseContainerOptions.swift diff --git a/Sources/SkyflowCore/core/container/Container.swift b/Sources/SkyflowCore/containers/Container.swift similarity index 100% rename from Sources/SkyflowCore/core/container/Container.swift rename to Sources/SkyflowCore/containers/Container.swift diff --git a/Sources/SkyflowCore/core/container/ContainerProtocol.swift b/Sources/SkyflowCore/containers/ContainerProtocol.swift similarity index 100% rename from Sources/SkyflowCore/core/container/ContainerProtocol.swift rename to Sources/SkyflowCore/containers/ContainerProtocol.swift diff --git a/Sources/SkyflowCore/core/container/ContainerType.swift b/Sources/SkyflowCore/containers/ContainerType.swift similarity index 100% rename from Sources/SkyflowCore/core/container/ContainerType.swift rename to Sources/SkyflowCore/containers/ContainerType.swift diff --git a/Sources/SkyflowCore/errors/ErrorCodes.swift b/Sources/SkyflowCore/errors/ErrorCodes.swift index d868c185..46308969 100644 --- a/Sources/SkyflowCore/errors/ErrorCodes.swift +++ b/Sources/SkyflowCore/errors/ErrorCodes.swift @@ -212,7 +212,7 @@ public class SkyflowError: NSError { // (Container.reveal()'s equivalent failures are already unwrapped earlier, // by RevealValueCallback.onFailure.) // - // A third shape reaches here too: FlowVaultInsertAPICallback's insert+update merge + // A third shape reaches here too: FlowVaultCollectAPICallback's insert+update merge // (group.notify in onSuccess) builds mergedErrors entries as {"error": {"message", // "httpCode", ...}} - a JSON-decoded API error detail dict, not an NSError - so it's // handled by trying SkyflowError(apiError:) on the entry itself, which expects exactly diff --git a/Sources/SkyflowCore/core/ContextOptions.swift b/Sources/SkyflowCore/logging/ContextOptions.swift similarity index 100% rename from Sources/SkyflowCore/core/ContextOptions.swift rename to Sources/SkyflowCore/logging/ContextOptions.swift diff --git a/Sources/SkyflowCore/core/EventName.swift b/Sources/SkyflowCore/logging/EventName.swift similarity index 100% rename from Sources/SkyflowCore/core/EventName.swift rename to Sources/SkyflowCore/logging/EventName.swift diff --git a/Sources/SkyflowCore/core/InterfaceName.swift b/Sources/SkyflowCore/logging/InterfaceName.swift similarity index 100% rename from Sources/SkyflowCore/core/InterfaceName.swift rename to Sources/SkyflowCore/logging/InterfaceName.swift diff --git a/Sources/SkyflowCore/core/Log.swift b/Sources/SkyflowCore/logging/Log.swift similarity index 100% rename from Sources/SkyflowCore/core/Log.swift rename to Sources/SkyflowCore/logging/Log.swift diff --git a/Sources/SkyflowCore/core/LogLevel.swift b/Sources/SkyflowCore/logging/LogLevel.swift similarity index 100% rename from Sources/SkyflowCore/core/LogLevel.swift rename to Sources/SkyflowCore/logging/LogLevel.swift diff --git a/Sources/SkyflowCore/core/Message.swift b/Sources/SkyflowCore/logging/Message.swift similarity index 100% rename from Sources/SkyflowCore/core/Message.swift rename to Sources/SkyflowCore/logging/Message.swift diff --git a/Sources/SkyflowCore/composable/AutoshiftAllowed.swift b/Sources/SkyflowCore/models/AutoshiftAllowed.swift similarity index 100% rename from Sources/SkyflowCore/composable/AutoshiftAllowed.swift rename to Sources/SkyflowCore/models/AutoshiftAllowed.swift diff --git a/Sources/SkyflowCore/elements/core/BaseCollectElementInput.swift b/Sources/SkyflowCore/models/BaseCollectElementInput.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/BaseCollectElementInput.swift rename to Sources/SkyflowCore/models/BaseCollectElementInput.swift diff --git a/Sources/SkyflowCore/elements/core/BaseCollectElementOptions.swift b/Sources/SkyflowCore/models/BaseCollectElementOptions.swift similarity index 100% rename from Sources/SkyflowCore/elements/core/BaseCollectElementOptions.swift rename to Sources/SkyflowCore/models/BaseCollectElementOptions.swift diff --git a/Sources/SkyflowCore/core/BaseConfiguration.swift b/Sources/SkyflowCore/models/BaseConfiguration.swift similarity index 100% rename from Sources/SkyflowCore/core/BaseConfiguration.swift rename to Sources/SkyflowCore/models/BaseConfiguration.swift diff --git a/Sources/SkyflowCore/reveal/BaseRevealElementInput.swift b/Sources/SkyflowCore/models/BaseRevealElementInput.swift similarity index 100% rename from Sources/SkyflowCore/reveal/BaseRevealElementInput.swift rename to Sources/SkyflowCore/models/BaseRevealElementInput.swift diff --git a/Sources/SkyflowCore/reveal/BaseRevealElementOptions.swift b/Sources/SkyflowCore/models/BaseRevealElementOptions.swift similarity index 100% rename from Sources/SkyflowCore/reveal/BaseRevealElementOptions.swift rename to Sources/SkyflowCore/models/BaseRevealElementOptions.swift diff --git a/Sources/SkyflowCore/reveal/RedactionType.swift b/Sources/SkyflowCore/models/RedactionType.swift similarity index 100% rename from Sources/SkyflowCore/reveal/RedactionType.swift rename to Sources/SkyflowCore/models/RedactionType.swift diff --git a/Sources/SkyflowCore/core/APIClient.swift b/Sources/SkyflowCore/networking/APIClient.swift similarity index 100% rename from Sources/SkyflowCore/core/APIClient.swift rename to Sources/SkyflowCore/networking/APIClient.swift diff --git a/Sources/SkyflowCore/collect/TokenAPICallback.swift b/Sources/SkyflowCore/networking/TokenAPICallback.swift similarity index 100% rename from Sources/SkyflowCore/collect/TokenAPICallback.swift rename to Sources/SkyflowCore/networking/TokenAPICallback.swift diff --git a/Sources/SkyflowCore/core/protocols/Callback.swift b/Sources/SkyflowCore/protocols/Callback.swift similarity index 100% rename from Sources/SkyflowCore/core/protocols/Callback.swift rename to Sources/SkyflowCore/protocols/Callback.swift diff --git a/Sources/SkyflowCore/core/protocols/TokenProvider.swift b/Sources/SkyflowCore/protocols/TokenProvider.swift similarity index 100% rename from Sources/SkyflowCore/core/protocols/TokenProvider.swift rename to Sources/SkyflowCore/protocols/TokenProvider.swift diff --git a/Sources/SkyflowCore/elements/utils/CardBin.swift b/Sources/SkyflowCore/utils/CardBin.swift similarity index 100% rename from Sources/SkyflowCore/elements/utils/CardBin.swift rename to Sources/SkyflowCore/utils/CardBin.swift diff --git a/Sources/SkyflowCore/elements/utils/CardType.swift b/Sources/SkyflowCore/utils/CardType.swift similarity index 100% rename from Sources/SkyflowCore/elements/utils/CardType.swift rename to Sources/SkyflowCore/utils/CardType.swift diff --git a/Sources/SkyflowCore/elements/utils/CharacterSet.swift b/Sources/SkyflowCore/utils/CharacterSet.swift similarity index 100% rename from Sources/SkyflowCore/elements/utils/CharacterSet.swift rename to Sources/SkyflowCore/utils/CharacterSet.swift diff --git a/Sources/SkyflowCore/reveal/ConversionHelpers.swift b/Sources/SkyflowCore/utils/ConversionHelpers.swift similarity index 100% rename from Sources/SkyflowCore/reveal/ConversionHelpers.swift rename to Sources/SkyflowCore/utils/ConversionHelpers.swift diff --git a/Sources/SkyflowCore/elements/validations/ElementValueMatchRule.swift b/Sources/SkyflowCore/validations/ElementValueMatchRule.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/ElementValueMatchRule.swift rename to Sources/SkyflowCore/validations/ElementValueMatchRule.swift diff --git a/Sources/SkyflowCore/elements/validations/LengthMatchRule.swift b/Sources/SkyflowCore/validations/LengthMatchRule.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/LengthMatchRule.swift rename to Sources/SkyflowCore/validations/LengthMatchRule.swift diff --git a/Sources/SkyflowCore/elements/validations/RegexMatchRule.swift b/Sources/SkyflowCore/validations/RegexMatchRule.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/RegexMatchRule.swift rename to Sources/SkyflowCore/validations/RegexMatchRule.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowInternalValidatorProtocol.swift b/Sources/SkyflowCore/validations/SkyflowInternalValidatorProtocol.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowInternalValidatorProtocol.swift rename to Sources/SkyflowCore/validations/SkyflowInternalValidatorProtocol.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidateCardExpirationDate.swift b/Sources/SkyflowCore/validations/SkyflowValidateCardExpirationDate.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidateCardExpirationDate.swift rename to Sources/SkyflowCore/validations/SkyflowValidateCardExpirationDate.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidateCardNumber.swift b/Sources/SkyflowCore/validations/SkyflowValidateCardNumber.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidateCardNumber.swift rename to Sources/SkyflowCore/validations/SkyflowValidateCardNumber.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationMonth.swift b/Sources/SkyflowCore/validations/SkyflowValidateExpirationMonth.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationMonth.swift rename to Sources/SkyflowCore/validations/SkyflowValidateExpirationMonth.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationYear.swift b/Sources/SkyflowCore/validations/SkyflowValidateExpirationYear.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidateExpirationYear.swift rename to Sources/SkyflowCore/validations/SkyflowValidateExpirationYear.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidateLengthMatch.swift b/Sources/SkyflowCore/validations/SkyflowValidateLengthMatch.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidateLengthMatch.swift rename to Sources/SkyflowCore/validations/SkyflowValidateLengthMatch.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidationError.swift b/Sources/SkyflowCore/validations/SkyflowValidationError.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidationError.swift rename to Sources/SkyflowCore/validations/SkyflowValidationError.swift diff --git a/Sources/SkyflowCore/elements/validations/SkyflowValidator.swift b/Sources/SkyflowCore/validations/SkyflowValidator.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/SkyflowValidator.swift rename to Sources/SkyflowCore/validations/SkyflowValidator.swift diff --git a/Sources/SkyflowCore/elements/validations/ValidationRule.swift b/Sources/SkyflowCore/validations/ValidationRule.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/ValidationRule.swift rename to Sources/SkyflowCore/validations/ValidationRule.swift diff --git a/Sources/SkyflowCore/elements/validations/ValidationSet.swift b/Sources/SkyflowCore/validations/ValidationSet.swift similarity index 100% rename from Sources/SkyflowCore/elements/validations/ValidationSet.swift rename to Sources/SkyflowCore/validations/ValidationSet.swift diff --git a/Sources/SkyflowFlowVaultIOS/Exports.swift b/Sources/SkyflowFlowVault/Exports.swift similarity index 85% rename from Sources/SkyflowFlowVaultIOS/Exports.swift rename to Sources/SkyflowFlowVault/Exports.swift index b24c9864..38de75ba 100644 --- a/Sources/SkyflowFlowVaultIOS/Exports.swift +++ b/Sources/SkyflowFlowVault/Exports.swift @@ -2,6 +2,6 @@ * Copyright (c) 2022 Skyflow */ -// Re-export the shared core so SDK consumers only need `import SkyflowFlowVaultIOS`. +// Re-export the shared core so SDK consumers only need `import SkyflowFlowVault`. @_exported import SkyflowCore diff --git a/Sources/SkyflowFlowVaultIOS/Init.swift b/Sources/SkyflowFlowVault/Init.swift similarity index 75% rename from Sources/SkyflowFlowVaultIOS/Init.swift rename to Sources/SkyflowFlowVault/Init.swift index b42b4afe..4aa7e559 100644 --- a/Sources/SkyflowFlowVaultIOS/Init.swift +++ b/Sources/SkyflowFlowVault/Init.swift @@ -2,7 +2,7 @@ * Copyright (c) 2022 Skyflow */ -// Initialize the FlowVault SDK, SkyflowFlowVaultIOS.initialize +// Initialize the FlowVault SDK, SkyflowFlowVault.initialize import Foundation diff --git a/Sources/SkyflowFlowVault/client/Client.swift b/Sources/SkyflowFlowVault/client/Client.swift new file mode 100644 index 00000000..ab601429 --- /dev/null +++ b/Sources/SkyflowFlowVault/client/Client.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// FlowVault (v2) entry points on the shared SkyflowCore Client class. +// The v2 operations live in ClientOperations.swift as further extensions. +// Future enhancements specific to this SDK go in extensions in this target +// without affecting the legacy Skyflow SDK. + +import Foundation + +public extension Client { + convenience init(_ skyflowConfig: Configuration) { + self.init(skyflowConfig.data) + } + + func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { + return makeContainer(type: type, options: options?.data) + } +} diff --git a/Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift b/Sources/SkyflowFlowVault/client/ClientOperations.swift similarity index 78% rename from Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift rename to Sources/SkyflowFlowVault/client/ClientOperations.swift index 4e69ead1..612f5264 100644 --- a/Sources/SkyflowFlowVaultIOS/core/ClientOperations.swift +++ b/Sources/SkyflowFlowVault/client/ClientOperations.swift @@ -11,15 +11,9 @@ extension Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .INSERT Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - let icOptions = FlowVaultICOptions(upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) var errorCode: ErrorCodes? if records["records"] == nil { @@ -60,9 +54,9 @@ extension Client { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return } else { - if options.upsert != nil { - if icOptions.validateUpsert() { - return; + if let upsert = options.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) } } let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, @@ -72,7 +66,7 @@ extension Client { onFailureHandler: { } ) - self.apiClient.post(records: records, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) + self.apiClient.post(records: records, callback: logCallback, upsert: options.upsert, contextOptions: tempContextOptions) } } else { errorCode = .INVALID_RECORDS_TYPE() @@ -83,24 +77,8 @@ extension Client { internal func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .DETOKENIZE - func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { - if token["token"] == nil { - return .ID_KEY_ERROR() - } else { - guard let _ = token["token"] as? String else { - return .INVALID_TOKEN_TYPE(value: "\(index)") - } - } - return nil - } - Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) @@ -115,7 +93,7 @@ extension Client { return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) } for (index,token) in tokens.enumerated() { - let errorCode = checkRecord(token: token, index: index) + let errorCode = RequestValidators.checkDetokenizeRecord(token: token, index: index) if errorCode == nil, let id = token["token"] as? String { list.append(RevealRequestRecord(token: id)) } else { diff --git a/Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift b/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift similarity index 63% rename from Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift rename to Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift index 95dd9c7d..055197f7 100644 --- a/Sources/SkyflowFlowVaultIOS/collect/CollectContainerOperations.swift +++ b/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift @@ -10,12 +10,7 @@ public extension Container { func collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -23,7 +18,7 @@ public extension Container { Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = checkElement(element: element) + errorCode = RequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return @@ -53,17 +48,16 @@ public extension Container { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } for (index, record) in additionalFields.records.enumerated() { - errorCode = checkRecord(record: record, index: index) + errorCode = RequestValidators.checkRecord(record: record, index: index) if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } } } - let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) - if options?.upsert != nil { - if icOptions.validateUpsert() { - return; + let records = CollectRequestBuilder.createCollectRecords(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + if let upsert = options?.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) } } if records != nil { @@ -76,32 +70,8 @@ public extension Container { ) let cvvMap = CVVTokenReplacer.captureCVVMap(elements: self.elements) let cvvMaskingCallback = CVVMaskingCallback(cvvMap: cvvMap, wrapping: logCallback) - self.skyflow.apiClient.postAndUpdate(records: records!, callback: cvvMaskingCallback, options: icOptions, contextOptions: tempContextOptions) - } - } - - private func checkElement(element: TextField) -> ErrorCodes? { - if element.collectInput.tableName.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) - } - - return nil - } - - private func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { - if record.tableName.isEmpty { - return .EMPTY_TABLE_NAME() - } - if record.data.isEmpty { - return .EMPTY_FIELDS_KEY(value: "\(index)") + self.skyflow.apiClient.postAndUpdate(records: records!, callback: cvvMaskingCallback, upsert: options?.upsert, contextOptions: tempContextOptions) } - return nil } } diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift b/Sources/SkyflowFlowVault/collect/CollectRequestBuilder.swift similarity index 83% rename from Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift rename to Sources/SkyflowFlowVault/collect/CollectRequestBuilder.swift index 306b3cda..c58baeda 100644 --- a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectRequestBody.swift +++ b/Sources/SkyflowFlowVault/collect/CollectRequestBuilder.swift @@ -2,11 +2,14 @@ * Copyright (c) 2022 Skyflow */ -// Class for formatting the request body for collecting the data (FlowDB v2) +// Builds the FlowDB v2 collect-side request bodies in stages: gathering the +// records from the mounted collect elements (plus additionalFields), then +// assembling the final insert / update-by-skyflowID wire payloads. +// Stateless builder: only static functions, never instantiated. import Foundation -internal class FlowVaultCollectRequestBody { +internal class CollectRequestBuilder { static var tableSet: Set = Set() static var callback: Callback? static var breakFlag = false @@ -32,7 +35,7 @@ internal class FlowVaultCollectRequestBody { } } } - + internal static func mergeFields(tableName: String, prefix: String, dict: [String: Any], contextOptions: ContextOptions) { for(key, val) in dict { let keypath = prefix == "" ? key : prefix + "." + key @@ -51,8 +54,10 @@ internal class FlowVaultCollectRequestBody { } } } - - internal static func createRequestBody( + + // Stage 1: gather the records from the mounted collect elements and + // additionalFields, split into insert ("records") and update buckets. + internal static func createCollectRecords( elements: [TextField], additionalFields: AdditionalFields? = nil, callback: Callback, @@ -202,4 +207,30 @@ internal class FlowVaultCollectRequestBody { } return ["records": payload, "update": updatePayload] } + + // Stage 2a: assemble the final v2 insert payload (with upsert options). + internal static func createInsertRequestBody(vaultID: String, records: [String: Any], upsert: [UpsertOption]? = nil) -> [String: Any] { + var recordsPayload: [[String: Any]] = [] + for record in (records["records"] as! [[String: Any]]) { + var temp: [String: Any] = [:] + temp["data"] = record["fields"] + if let tableName = record["table"] as? String { + temp["tableName"] = tableName + if let upsertOptions = upsert, let match = upsertOptions.first(where: { $0.tableName == tableName }) { + var upsertPayload: [String: Any] = ["uniqueColumns": match.uniqueColumns] + if let updateType = match.updateType { + upsertPayload["updateType"] = updateType.rawValue + } + temp["upsert"] = upsertPayload + } + } + recordsPayload.append(temp) + } + return ["vaultID": vaultID, "records": recordsPayload] + } + + // Stage 2b: assemble the final v2 update-by-skyflowID payload. + internal static func createUpdateRequestBody(vaultID: String, records: [[String: Any]]) -> [String: Any] { + return ["vaultID": vaultID, "records": records] + } } diff --git a/Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift b/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift similarity index 63% rename from Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift rename to Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift index e231f431..a253dbe5 100644 --- a/Sources/SkyflowFlowVaultIOS/collect/ComposableContainerOperations.swift +++ b/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift @@ -8,16 +8,10 @@ import Foundation import UIKit public extension Container { - func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -25,7 +19,7 @@ public extension Container { Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = checkComposableElement(element: element) + errorCode = RequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return @@ -57,17 +51,16 @@ public extension Container { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } for (index, record) in additionalFields.records.enumerated() { - errorCode = checkComposableRecord(record: record, index: index) + errorCode = RequestValidators.checkRecord(record: record, index: index) if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } } } - let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) - if options?.upsert != nil { - if icOptions.validateUpsert() { - return; + let records = CollectRequestBuilder.createCollectRecords(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + if let upsert = options?.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) } } if records != nil { @@ -80,32 +73,8 @@ public extension Container { ) let cvvMap = CVVTokenReplacer.captureCVVMap(elements: self.elements) let cvvMaskingCallback = CVVMaskingCallback(cvvMap: cvvMap, wrapping: logCallback) - self.skyflow.apiClient.postAndUpdate(records: records!, callback: cvvMaskingCallback, options: icOptions, contextOptions: tempContextOptions) - } - } - - private func checkComposableElement(element: TextField) -> ErrorCodes? { - if element.collectInput.tableName.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + self.skyflow.apiClient.postAndUpdate(records: records!, callback: cvvMaskingCallback, upsert: options?.upsert, contextOptions: tempContextOptions) } - - return nil - } - - private func checkComposableRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { - if record.tableName.isEmpty { - return .EMPTY_TABLE_NAME() - } - if record.data.isEmpty { - return .EMPTY_FIELDS_KEY(value: "\(index)") - } - return nil } } diff --git a/Sources/SkyflowFlowVaultIOS/core/ContainerOptions.swift b/Sources/SkyflowFlowVault/composable/ContainerOptions.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/core/ContainerOptions.swift rename to Sources/SkyflowFlowVault/composable/ContainerOptions.swift diff --git a/Sources/SkyflowFlowVaultIOS/collect/AdditionalFields.swift b/Sources/SkyflowFlowVault/models/AdditionalFields.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/collect/AdditionalFields.swift rename to Sources/SkyflowFlowVault/models/AdditionalFields.swift diff --git a/Sources/SkyflowFlowVaultIOS/elements/CollectElementInput.swift b/Sources/SkyflowFlowVault/models/CollectElementInput.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/elements/CollectElementInput.swift rename to Sources/SkyflowFlowVault/models/CollectElementInput.swift diff --git a/Sources/SkyflowFlowVaultIOS/elements/CollectElementOptions.swift b/Sources/SkyflowFlowVault/models/CollectElementOptions.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/elements/CollectElementOptions.swift rename to Sources/SkyflowFlowVault/models/CollectElementOptions.swift diff --git a/Sources/SkyflowFlowVaultIOS/collect/CollectOptions.swift b/Sources/SkyflowFlowVault/models/CollectOptions.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/collect/CollectOptions.swift rename to Sources/SkyflowFlowVault/models/CollectOptions.swift diff --git a/Sources/SkyflowFlowVaultIOS/collect/CollectResponse.swift b/Sources/SkyflowFlowVault/models/CollectResponse.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/collect/CollectResponse.swift rename to Sources/SkyflowFlowVault/models/CollectResponse.swift diff --git a/Sources/SkyflowFlowVaultIOS/core/Configuration.swift b/Sources/SkyflowFlowVault/models/Configuration.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/core/Configuration.swift rename to Sources/SkyflowFlowVault/models/Configuration.swift diff --git a/Sources/SkyflowFlowVaultIOS/collect/InsertOptions.swift b/Sources/SkyflowFlowVault/models/InsertOptions.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/collect/InsertOptions.swift rename to Sources/SkyflowFlowVault/models/InsertOptions.swift diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealElementInput.swift b/Sources/SkyflowFlowVault/models/RevealElementInput.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealElementInput.swift rename to Sources/SkyflowFlowVault/models/RevealElementInput.swift diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealElementOptions.swift b/Sources/SkyflowFlowVault/models/RevealElementOptions.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealElementOptions.swift rename to Sources/SkyflowFlowVault/models/RevealElementOptions.swift diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealOptions.swift b/Sources/SkyflowFlowVault/models/RevealOptions.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealOptions.swift rename to Sources/SkyflowFlowVault/models/RevealOptions.swift diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestRecord.swift b/Sources/SkyflowFlowVault/models/RevealRequestRecord.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealRequestRecord.swift rename to Sources/SkyflowFlowVault/models/RevealRequestRecord.swift diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealResponse.swift b/Sources/SkyflowFlowVault/models/RevealResponse.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealResponse.swift rename to Sources/SkyflowFlowVault/models/RevealResponse.swift diff --git a/Sources/SkyflowFlowVaultIOS/reveal/TokenGroupRedaction.swift b/Sources/SkyflowFlowVault/models/TokenGroupRedaction.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/TokenGroupRedaction.swift rename to Sources/SkyflowFlowVault/models/TokenGroupRedaction.swift diff --git a/Sources/SkyflowFlowVaultIOS/collect/UpsertOption.swift b/Sources/SkyflowFlowVault/models/UpsertOption.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/collect/UpsertOption.swift rename to Sources/SkyflowFlowVault/models/UpsertOption.swift diff --git a/Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift b/Sources/SkyflowFlowVault/networking/APIClientFlowVault.swift similarity index 71% rename from Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift rename to Sources/SkyflowFlowVault/networking/APIClientFlowVault.swift index 9ccbb1be..ca0a469c 100644 --- a/Sources/SkyflowFlowVaultIOS/core/APIClientFlowVault.swift +++ b/Sources/SkyflowFlowVault/networking/APIClientFlowVault.swift @@ -7,12 +7,12 @@ import Foundation extension APIClient { - internal func postAndUpdate(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { - let collectApiCallback = FlowVaultCollectAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + internal func postAndUpdate(records: [String: Any], callback: Callback, upsert: [UpsertOption]? = nil, contextOptions: ContextOptions) { + let collectApiCallback = FlowVaultCollectAPICallback(callback: callback, apiClient: self, records: records, upsert: upsert, contextOptions: contextOptions) self.getAccessToken(callback: collectApiCallback, contextOptions: contextOptions) } - internal func post(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { - let insertApiCallback = FlowVaultInsertAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + internal func post(records: [String: Any], callback: Callback, upsert: [UpsertOption]? = nil, contextOptions: ContextOptions) { + let insertApiCallback = FlowVaultCollectAPICallback(callback: callback, apiClient: self, records: records, upsert: upsert, contextOptions: contextOptions) self.getAccessToken(callback: insertApiCallback, contextOptions: contextOptions) } diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift b/Sources/SkyflowFlowVault/networking/FlowVaultCollectAPICallback.swift similarity index 93% rename from Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift rename to Sources/SkyflowFlowVault/networking/FlowVaultCollectAPICallback.swift index ebfdb0a9..85df4d5a 100644 --- a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultCollectAPICallback.swift +++ b/Sources/SkyflowFlowVault/networking/FlowVaultCollectAPICallback.swift @@ -8,17 +8,22 @@ import Foundation import UIKit internal class FlowVaultCollectAPICallback: Callback { + // Overridable only for tests (e.g. injecting a URLProtocol mock via protocolClasses) - global + // URLProtocol.registerClass(_:) isn't reliably consulted for manually-created URLSession + // instances in every environment, so tests need a real seam here instead. + internal static var urlSessionConfiguration: URLSessionConfiguration = .default + var apiClient: APIClient var records: [String: Any] var callback: Callback - var options: FlowVaultICOptions + var upsert: [UpsertOption]? var contextOptions: ContextOptions - internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], upsert: [UpsertOption]? = nil, contextOptions: ContextOptions) { self.records = records self.apiClient = apiClient self.callback = callback - self.options = options + self.upsert = upsert self.contextOptions = contextOptions } @@ -169,7 +174,7 @@ internal class FlowVaultCollectAPICallback: Callback { request.httpMethod = "POST" do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + let data = try JSONSerialization.data(withJSONObject: CollectRequestBuilder.createInsertRequestBody(vaultID: self.apiClient.vaultID, records: self.records, upsert: self.upsert)) request.httpBody = data } @@ -178,7 +183,7 @@ internal class FlowVaultCollectAPICallback: Callback { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - return (request, URLSession(configuration: .default)) + return (request, URLSession(configuration: Self.urlSessionConfiguration)) } @@ -196,7 +201,7 @@ internal class FlowVaultCollectAPICallback: Callback { request.httpMethod = "POST" do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) + let data = try JSONSerialization.data(withJSONObject: CollectRequestBuilder.createUpdateRequestBody(vaultID: self.apiClient.vaultID, records: records)) request.httpBody = data } @@ -205,7 +210,7 @@ internal class FlowVaultCollectAPICallback: Callback { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - return (request, URLSession(configuration: .default)) + return (request, URLSession(configuration: Self.urlSessionConfiguration)) } func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { diff --git a/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift b/Sources/SkyflowFlowVault/networking/FlowVaultRevealApiCallback.swift similarity index 99% rename from Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift rename to Sources/SkyflowFlowVault/networking/FlowVaultRevealApiCallback.swift index 7932295e..e6f613c4 100644 --- a/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultRevealApiCallback.swift +++ b/Sources/SkyflowFlowVault/networking/FlowVaultRevealApiCallback.swift @@ -75,7 +75,7 @@ class FlowVaultRevealAPICallback: Callback { request.httpMethod = "POST" do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records, tokenGroupRedactions: tokenGroupRedactions)) + let data = try JSONSerialization.data(withJSONObject: RevealRequestBuilder.createDetokenizeRequestBody(vaultID: self.apiClient.vaultID, records: records, tokenGroupRedactions: tokenGroupRedactions)) request.httpBody = data } diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift b/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift similarity index 57% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift rename to Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift index 269cade6..15dc805b 100644 --- a/Sources/SkyflowFlowVaultIOS/reveal/RevealContainerOperations.swift +++ b/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift @@ -11,12 +11,7 @@ public extension Container { func reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errorCode: ErrorCodes? @@ -26,34 +21,18 @@ public extension Container { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return } - for element in self.revealElements { - if element.errorTriggered { - errorCode = .ERROR_TRIGGERED(value: element.triggeredErrorMessage) - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - if element.getToken().isEmpty { - errorCode = .EMPTY_TOKEN_ID() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } + if let elementError = RequestValidators.checkRevealElements(elements: self.revealElements) { + callback.onFailure(elementError.getErrorObject(contextOptions: tempContextOptions)) + return } - // Mirrors the JS SDK's validateRevealOptions: every redaction entry must have a - // non-empty tokenGroupName and redaction. Whitespace-only values are rejected - // too — stricter than JS, which only checks !== ''. (The JS array/string type - // checks are enforced by Swift's type system and don't need porting.) if let redactions = options?.tokenGroupRedactions { - for (index, entry) in redactions.enumerated() { - if entry.tokenGroupName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - || entry.redaction.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - errorCode = .INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: String(index)) - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } + if let redactionError = RequestValidators.checkTokenGroupRedactions(redactions) { + callback.onFailure(redactionError.getErrorObject(contextOptions: tempContextOptions)) + return } } let revealValueCallback = RevealValueCallback(callback: callback, revealElements: self.revealElements, contextOptions: tempContextOptions) - let records = RevealRequestBody.createRequestBody(elements: self.revealElements) + let records = RevealRequestBuilder.createRevealRecords(elements: self.revealElements) if let tokens = records["records"] as? [[String: Any]] { var list: [RevealRequestRecord] = [] diff --git a/Sources/SkyflowFlowVault/reveal/RevealRequestBuilder.swift b/Sources/SkyflowFlowVault/reveal/RevealRequestBuilder.swift new file mode 100644 index 00000000..869f9229 --- /dev/null +++ b/Sources/SkyflowFlowVault/reveal/RevealRequestBuilder.swift @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Builds the FlowDB v2 detokenize request in two stages: gathering the tokens +// from the mounted reveal elements, then assembling the final wire payload. +// Stateless builder: only static functions, never instantiated. + +import Foundation + +internal class RevealRequestBuilder { + // Stage 1: gather the tokens from the mounted reveal elements. + internal static func createRevealRecords(elements: [Label]) -> [String: Any] { + var payload: [[String: Any]] = [] + for element in elements { + var entry: [String: Any] = [:] + entry["token"] = element.revealInput.token + payload.append(entry) + } + + return ["records": payload] + } + + // Stage 2: assemble the final v2 detokenize payload. + internal static func createDetokenizeRequestBody(vaultID: String, records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil) -> [String: Any] { + let tokens = records.map { $0.token } + var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] + if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { + // Pass through exactly what was collected from the mounted elements, duplicates + // and all - the API validates tokenGroupRedactions, not the SDK. + body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } + } + return body + } +} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealValueCallback.swift b/Sources/SkyflowFlowVault/reveal/RevealValueCallback.swift similarity index 100% rename from Sources/SkyflowFlowVaultIOS/reveal/RevealValueCallback.swift rename to Sources/SkyflowFlowVault/reveal/RevealValueCallback.swift diff --git a/Sources/SkyflowFlowVaultIOS/collect/CVVTokenReplacer.swift b/Sources/SkyflowFlowVault/utils/CVVTokenReplacer.swift similarity index 86% rename from Sources/SkyflowFlowVaultIOS/collect/CVVTokenReplacer.swift rename to Sources/SkyflowFlowVault/utils/CVVTokenReplacer.swift index 3ce86f4c..80c610c2 100644 --- a/Sources/SkyflowFlowVaultIOS/collect/CVVTokenReplacer.swift +++ b/Sources/SkyflowFlowVault/utils/CVVTokenReplacer.swift @@ -21,7 +21,7 @@ internal enum CVVTokenReplacer { // "" too (see applyMock below), rather than leaking the real vault token unmasked. // // First element wins per (table/recordId, column) - mirrors the existing dedup behavior in - // FlowVaultCollectRequestBody, where a second element sharing a column (ElementValueMatchRule) + // CollectRequestBuilder, where a second element sharing a column (ElementValueMatchRule) // is dropped rather than merged. internal static func captureCVVMap(elements: [TextField]) -> CVVCaptureMap { var map = CVVCaptureMap() @@ -130,3 +130,25 @@ internal class CVVMaskingCallback: Callback { return dict } } +internal enum CVVMockGenerator { + internal static func generateMockCVV(length: Int, actualValue: String) -> String { + guard length > 0 else { return "" } + var mock: String + repeat { + mock = randomNumericString(length: length) + } while mock == actualValue + return mock + } + + private static func randomNumericString(length: Int) -> String { + var bytes = [UInt8](repeating: 0, count: length) + let status = SecRandomCopyBytes(kSecRandomDefault, length, &bytes) + if status != errSecSuccess { + var generator = SystemRandomNumberGenerator() + for i in 0.. ErrorCodes? { + if vaultID.isEmpty { + return .EMPTY_VAULT_ID() + } + if vaultURL == "/" { + return .EMPTY_VAULT_URL() + } + return nil + } + + internal static func checkElement(element: TextField) -> ErrorCodes? { + if element.collectInput.tableName.isEmpty { + return .EMPTY_TABLE_NAME_IN_COLLECT() + } + if element.collectInput.column.isEmpty { + return .EMPTY_COLUMN_NAME_IN_COLLECT() + } + if !element.isMounted() { + return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + } + + return nil + } + + internal static func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.tableName.isEmpty { + return .EMPTY_TABLE_NAME() + } + if record.data.isEmpty { + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + return nil + } + + internal static func checkRevealElements(elements: [Label]) -> ErrorCodes? { + for element in elements { + if element.errorTriggered { + return .ERROR_TRIGGERED(value: element.triggeredErrorMessage) + } + if element.getToken().isEmpty { + return .EMPTY_TOKEN_ID() + } + } + return nil + } + + internal static func checkTokenGroupRedactions(_ redactions: [TokenGroupRedaction]) -> ErrorCodes? { + for (index, entry) in redactions.enumerated() { + if entry.tokenGroupName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + || entry.redaction.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return .INVALID_TOKEN_GROUP_REDACTION_ENTRY(value: String(index)) + } + } + return nil + } + + internal static func checkUpsertOptions(_ upsert: [UpsertOption]) -> ErrorCodes? { + if upsert.count == 0 { + return .UPSERT_OPTION_CANNOT_BE_EMPTY() + } + for (index, currUpsertOption) in upsert.enumerated() { + if currUpsertOption.tableName == "" { + return .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + } + if currUpsertOption.uniqueColumns.isEmpty { + return .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + } + } + return nil + } + + internal static func checkDetokenizeRecord(token: [String: Any], index: Int) -> ErrorCodes? { + if token["token"] == nil { + return .ID_KEY_ERROR() + } else { + guard let _ = token["token"] as? String else { + return .INVALID_TOKEN_TYPE(value: "\(index)") + } + } + return nil + } +} diff --git a/Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift b/Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift deleted file mode 100644 index 73465dd8..00000000 --- a/Sources/SkyflowFlowVaultIOS/collect/CVVMockGenerator.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Generates a mock CVV placeholder used to mask the real CVV token in collect responses (FlowDB v2). - -import Foundation - -internal enum CVVMockGenerator { - internal static func generateMockCVV(length: Int, actualValue: String) -> String { - guard length > 0 else { return "" } - var mock: String - repeat { - mock = randomNumericString(length: length) - } while mock == actualValue - return mock - } - - private static func randomNumericString(length: Int) -> String { - var bytes = [UInt8](repeating: 0, count: length) - let status = SecRandomCopyBytes(kSecRandomDefault, length, &bytes) - if status != errSecSuccess { - var generator = SystemRandomNumberGenerator() - for i in 0.. Bool{ - if self.upsert != nil { - if self.upsert!.count == 0 { - let errorCode = ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY() - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - - for (index, currUpsertOption) in self.upsert!.enumerated() { - if currUpsertOption.tableName == "" { - let errorCode = ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - if currUpsertOption.uniqueColumns.isEmpty { - let errorCode = ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - } - - } - return false; - } -} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift deleted file mode 100644 index 22a99ac2..00000000 --- a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertAPICallback.swift +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Callback used while API callback for Collect the elements (FlowDB v2) - -import Foundation -import UIKit - -internal class FlowVaultInsertAPICallback: Callback { - // Overridable only for tests (e.g. injecting a URLProtocol mock via protocolClasses) - global - // URLProtocol.registerClass(_:) isn't reliably consulted for manually-created URLSession - // instances in every environment, so tests need a real seam here instead. - internal static var urlSessionConfiguration: URLSessionConfiguration = .default - var apiClient: APIClient - var records: [String: Any] - var callback: Callback - var options: FlowVaultICOptions - var contextOptions: ContextOptions - - internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { - self.records = records - self.apiClient = apiClient - self.callback = callback - self.options = options - self.contextOptions = contextOptions - } - internal func onSuccess(_ responseBody: Any) { - let insertRecords = records["records"] as? [[String: Any]] ?? [] - let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) - let hasInsert = !insertRecords.isEmpty - let hasUpdate = !updateRecords.isEmpty - - if !hasInsert && !hasUpdate { - self.callback.onSuccess(["records": []]) - return - } - - // No update records: preserve the original single-call insert behavior exactly - // (including throwing a full-request-level failure straight to onFailure). - if !hasUpdate { - guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { - self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) - return - } - do { - let (request, session) = try self.getRequestSession(url: url) - let task = session.dataTask(with: request) { data, response, error in - do { - let response = try self.processResponse(data: data, response: response, error: error) - if response["error"] != nil { - self.callback.onFailure(response) - return - } - self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) - } catch { - self.callback.onFailure(error) - } - } - task.resume() - } catch let error { - self.callback.onFailure(error) - } - return - } - - guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { - self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) - return - } - - let group = DispatchGroup() - var mergedRecords: [[String: Any]] = [] - var mergedErrors: [[String: Any]] = [] - - if hasInsert { - group.enter() - let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! - do { - let (request, session) = try self.getRequestSession(url: url) - let task = session.dataTask(with: request) { data, response, error in - defer { group.leave() } - do { - let response = try self.processResponse(data: data, response: response, error: error) - if response["error"] != nil { - mergedErrors.append(response) - } else { - mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) - } - } catch { - mergedErrors.append(["error": error.localizedDescription]) - } - } - task.resume() - } catch let error { - mergedErrors.append(["error": error.localizedDescription]) - group.leave() - } - } - - if hasUpdate { - group.enter() - let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! - do { - let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) - let task = session.dataTask(with: request) { data, response, error in - defer { group.leave() } - do { - let response = try self.processResponse(data: data, response: response, error: error) - if response["error"] != nil { - mergedErrors.append(response) - } else { - mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) - } - } catch { - mergedErrors.append(["error": error.localizedDescription]) - } - } - task.resume() - } catch let error { - mergedErrors.append(["error": error.localizedDescription]) - group.leave() - } - } - - group.notify(queue: .main) { - if mergedErrors.isEmpty { - self.callback.onSuccess(["records": mergedRecords]) - } else { - self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) - } - } - } - - internal func onFailure(_ error: Any) { - self.callback.onFailure(error) - } - - internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { - var updateRecords: [[String: Any]] = [] - for (skyflowID, value) in updateDict { - guard let entry = value as? [String: Any], - let tableName = entry["table"] as? String, - let fields = entry["fields"] as? [String: Any] else { continue } - updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) - } - return updateRecords - } - - internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { - var temp: [String: Any] = [:] - for (key, val) in dict { - if let v = val as? [String: Any] { - temp[key] = buildFieldsDict(dict: v) - } else { - temp[key] = val - } - } - return temp - } - internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { - var jsonString = "" - - do { - let deviceDetails = FetchMetrices().getMetrices() - let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) - jsonString = String(data: jsonData, encoding: .utf8) ?? "" - } catch { - jsonString = "" - } - var request = URLRequest(url: url) - request.httpMethod = "POST" - - do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) - request.httpBody = data - } - - request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("application/json", forHTTPHeaderField: "Accept") - request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - - return (request, URLSession(configuration: Self.urlSessionConfiguration)) - - } - - internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { - var jsonString = "" - - do { - let deviceDetails = FetchMetrices().getMetrices() - let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) - jsonString = String(data: jsonData, encoding: .utf8) ?? "" - } catch { - jsonString = "" - } - var request = URLRequest(url: url) - request.httpMethod = "POST" - - do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) - request.httpBody = data - } - - request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("application/json", forHTTPHeaderField: "Accept") - request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - - return (request, URLSession(configuration: Self.urlSessionConfiguration)) - } - - func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { - if error != nil || response == nil { - return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] - } - - if let httpResponse = response as? HTTPURLResponse { - let range = 400...599 - if range ~= httpResponse.statusCode { - // FlowDB returns a non-2xx status when every record in the batch fails, - // but the body still has the same {"records": [...]} shape as a success/partial - // response (each record carrying its own error/httpCode) - parse it as such. - if let safeData = data, - let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], - jsonObject["records"] != nil { - return try getCollectResponseBody(data: safeData) - } - var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) - if let safeData = data { - guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { - return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] - } - // Pass the vault's structured error object (grpcCode/httpStatus/details) through - // as-is, only patching in the request-id and defaulting httpCode when absent. - if var errorDict = errorResponse["error"] as? [String: Any] { - if let message = errorDict["message"] as? String { - description = message - if let requestId = httpResponse.allHeaderFields["x-request-id"] { - description += " - request-id: \(requestId)" - } - errorDict["message"] = description - } - if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } - return ["error": errorDict] - } - if let requestId = httpResponse.allHeaderFields["x-request-id"] { - description += " - request-id: \(requestId)" - } - } - return ["error": ["message": description, "httpCode": httpResponse.statusCode]] - } - } - - guard let safeData = data else { - return ["records": []] - } - - return try getCollectResponseBody(data: safeData) - - } - - func getCollectResponseBody(data: Data) throws -> [String: Any]{ - let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] - var records: [[String: Any]] = [] - - let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] - for entry in responseRecords { - if let error = entry["error"] as? String { - var errorEntry: [String: Any] = ["error": error] - if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } - if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } - if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } - records.append(errorEntry) - } else { - var successEntry: [String: Any] = [:] - if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } - if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } - var fields: [String: Any] = [:] - if let tokens = entry["tokens"] as? [String: Any] { - for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { - fields[column] = tokenValue - } - } - successEntry["fields"] = fields - if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } - if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } - records.append(successEntry) - } - } - - return ["records": records] - } -} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift deleted file mode 100644 index 0c35872e..00000000 --- a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultInsertRequestBody.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Used for generating request body for FlowDB v2 insert api call - -import Foundation - -internal class FlowVaultInsertRequestBody { - internal static func createRequestBody(vaultID: String, records: [String: Any], options: FlowVaultICOptions) -> [String: Any] { - var recordsPayload: [[String: Any]] = [] - for record in (records["records"] as! [[String: Any]]) { - var temp: [String: Any] = [:] - temp["data"] = record["fields"] - if let tableName = record["table"] as? String { - temp["tableName"] = tableName - if let upsertOptions = options.upsert, let match = upsertOptions.first(where: { $0.tableName == tableName }) { - var upsertPayload: [String: Any] = ["uniqueColumns": match.uniqueColumns] - if let updateType = match.updateType { - upsertPayload["updateType"] = updateType.rawValue - } - temp["upsert"] = upsertPayload - } - } - recordsPayload.append(temp) - } - return ["vaultID": vaultID, "records": recordsPayload] - } -} diff --git a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift b/Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift deleted file mode 100644 index 54187810..00000000 --- a/Sources/SkyflowFlowVaultIOS/collect/FlowVaultUpdateRequestBody.swift +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Used for generating request body for FlowDB v2 update-by-skyflowID api call - -import Foundation - -internal class FlowVaultUpdateRequestBody { - internal static func createRequestBody(vaultID: String, records: [[String: Any]]) -> [String: Any] { - return ["vaultID": vaultID, "records": records] - } -} diff --git a/Sources/SkyflowFlowVaultIOS/core/Client.swift b/Sources/SkyflowFlowVaultIOS/core/Client.swift deleted file mode 100644 index 94de1c23..00000000 --- a/Sources/SkyflowFlowVaultIOS/core/Client.swift +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Implementation of the Skyflow Client class (FlowVault v2 contract). -// The v2 operations live in ClientOperations.swift as an extension of this -// class. Future enhancements specific to this SDK go here (or in extensions -// in this target) without affecting the legacy Skyflow SDK. - -import Foundation - -public class Client: ClientBase { - public init(_ skyflowConfig: Configuration) { - super.init(skyflowConfig.data) - } -} - -public extension Client { - func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { - return makeContainer(type: type, options: options?.data) - } -} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift b/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift deleted file mode 100644 index 0a689f8d..00000000 --- a/Sources/SkyflowFlowVaultIOS/reveal/FlowVaultDetokenizeRequestBody.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Used for generating request body for FlowDB v2 detokenize api call - -import Foundation - -internal class FlowVaultDetokenizeRequestBody { - internal static func createRequestBody(vaultID: String, records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil) -> [String: Any] { - let tokens = records.map { $0.token } - var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] - if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { - // Pass through exactly what was collected from the mounted elements, duplicates - // and all - the API validates tokenGroupRedactions, not the SDK. - body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } - } - return body - } -} diff --git a/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift b/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift deleted file mode 100644 index 8b0230e1..00000000 --- a/Sources/SkyflowFlowVaultIOS/reveal/RevealRequestBody.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Used for generating request body for reveal api call - -import Foundation - -internal class RevealRequestBody { - internal static func createRequestBody(elements: [Label]) -> [String: Any] { - var payload: [[String: Any]] = [] - for element in elements { - var entry: [String: Any] = [:] - entry["token"] = element.revealInput.token - payload.append(entry) - } - - return ["records": payload] - } -} diff --git a/Tests/skyflow-iOS-collectTests/DemoImpl.swift b/Tests/skyflow-iOS-collectTests/DemoImpl.swift index 094be014..28e6485d 100644 --- a/Tests/skyflow-iOS-collectTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-collectTests/DemoImpl.swift @@ -11,7 +11,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class DemoTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index cdee80fc..6647a325 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -3,7 +3,7 @@ */ import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -11,7 +11,7 @@ final class skyflow_iOS_collectTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = SkyflowFlowVaultIOS.initialize( + self.skyflow = SkyflowFlowVault.initialize( Configuration( vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, @@ -580,7 +580,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectSuccess = XCTestExpectation(description: "Should succeed") let myCallback = DemoAPICallback(expectation: expectSuccess) - let records = FlowVaultCollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) + let records = CollectRequestBuilder.createCollectRecords(elements: elements, callback: myCallback, contextOptions: ContextOptions()) let recordElement = (records?["records"] as? [[String: Any]])?[0] let fields = recordElement?["fields"] as? [String: Any] diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift index 6c69a2ab..72450e56 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift @@ -2,12 +2,12 @@ * Copyright (c) 2022 Skyflow */ -// Unit tests for the EMPTY_SKYFLOW_ID validation in FlowVaultCollectRequestBody: +// Unit tests for the EMPTY_SKYFLOW_ID validation in CollectRequestBuilder: // an explicit empty-string skyflowId (on an element or an additionalFields record) // is rejected client-side instead of silently falling back to an insert. import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { @@ -38,7 +38,7 @@ final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { AdditionalFieldsRecord(tableName: "persons", data: ["name": "demo"], skyflowId: "") ]) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNil(requestBody) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index 0").description) @@ -51,7 +51,7 @@ final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { AdditionalFieldsRecord(tableName: "persons", data: ["age": "30"], skyflowId: "") ]) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNil(requestBody) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index 1").description) @@ -61,7 +61,7 @@ final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { let element = makeNameElement(skyflowId: "") let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId on element")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [element], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [element], callback: callback, contextOptions: ContextOptions()) XCTAssertNil(requestBody) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "element with column 'name'").description) @@ -74,7 +74,7 @@ final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { AdditionalFieldsRecord(tableName: "table1", data: ["email": "a@b.com"]) ]) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [element], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [element], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let records = requestBody?["records"] as! [[String: Any]] @@ -92,7 +92,7 @@ final class skyflow_iOS_emptySkyflowIdValidationTests: XCTestCase { let element = makeNameElement(skyflowId: "row-1") let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Valid skyflowId updates")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [element], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [element], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) diff --git a/Tests/skyflow-iOS-composableTests/DemoImpl.swift b/Tests/skyflow-iOS-composableTests/DemoImpl.swift index b8994e2f..5b9aa5d6 100644 --- a/Tests/skyflow-iOS-composableTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-composableTests/DemoImpl.swift @@ -7,7 +7,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class DemoTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index 885b137a..e000e6ae 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -6,14 +6,14 @@ // import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore final class skyflow_iOS_composableEelementsTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = SkyflowFlowVaultIOS.initialize( + self.skyflow = SkyflowFlowVault.initialize( Configuration( vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, diff --git a/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift b/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift index 8aa1d148..0232177e 100644 --- a/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift +++ b/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift @@ -6,7 +6,7 @@ // import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore class CollectElementOptionsTests: XCTestCase { diff --git a/Tests/skyflow-iOS-elementTests/DemoImpl.swift b/Tests/skyflow-iOS-elementTests/DemoImpl.swift index 0f86e36e..d9030634 100644 --- a/Tests/skyflow-iOS-elementTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-elementTests/DemoImpl.swift @@ -4,7 +4,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class DemoTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift b/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift index 74e93bc0..7d87e07d 100644 --- a/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift +++ b/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift @@ -4,7 +4,7 @@ import XCTest import Foundation -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -13,7 +13,7 @@ class InputFormattingTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = SkyflowFlowVaultIOS.initialize( + self.skyflow = SkyflowFlowVault.initialize( Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, tokenProvider: DemoTokenProvider()) diff --git a/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift b/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift index b1caad42..7571a7de 100644 --- a/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift +++ b/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift @@ -7,7 +7,7 @@ import XCTest import Foundation -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -16,7 +16,7 @@ class TextFieldDelegateTests: XCTestCase { var skyflow: Client! override func setUp() { - self.skyflow = SkyflowFlowVaultIOS.initialize( + self.skyflow = SkyflowFlowVault.initialize( Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, tokenProvider: DemoTokenProvider()) diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift b/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift index d8983c01..102b47dc 100644 --- a/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift +++ b/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift @@ -4,7 +4,7 @@ import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift index 4e9b0c26..cd903c4f 100644 --- a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift +++ b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift @@ -5,7 +5,7 @@ import XCTest import UIKit -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length diff --git a/Tests/skyflow-iOS-errorTests/DemoImpl.swift b/Tests/skyflow-iOS-errorTests/DemoImpl.swift index a9ac3d2a..f9ae2008 100644 --- a/Tests/skyflow-iOS-errorTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-errorTests/DemoImpl.swift @@ -4,7 +4,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class DemoTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-errorTests/GatewayImpl.swift b/Tests/skyflow-iOS-errorTests/GatewayImpl.swift index 9439d630..949cb438 100644 --- a/Tests/skyflow-iOS-errorTests/GatewayImpl.swift +++ b/Tests/skyflow-iOS-errorTests/GatewayImpl.swift @@ -4,7 +4,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class ConnectionTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 4d80b75d..5b6a29dd 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -12,7 +12,7 @@ import XCTest import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -268,7 +268,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - Duplicate Elements") let callback = DemoAPICallback(expectation: expectation) - FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + CollectRequestBuilder.createCollectRecords(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -294,7 +294,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let fields = AdditionalFields(records: [ AdditionalFieldsRecord(tableName: "persons", data: ["cvv": "123", "name": "John Doe"]) ]) - FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + CollectRequestBuilder.createCollectRecords(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -321,7 +321,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { AdditionalFieldsRecord(tableName: "persons", data: ["duplicate": "123", "name": "John Doe"]), AdditionalFieldsRecord(tableName: "persons", data: ["duplicate": "123"]) ]) - FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + CollectRequestBuilder.createCollectRecords(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -347,7 +347,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { AdditionalFieldsRecord(tableName: "persons", data: ["cvv": "123"]), AdditionalFieldsRecord(tableName: "persons", data: ["duplicate": "123"]) ]) - FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + CollectRequestBuilder.createCollectRecords(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -368,7 +368,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { cvv?.textField.secureText = "211" window.addSubview(cvv!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertNotNil(requestBody?["records"]) XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ @@ -391,7 +391,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cardNumber!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Explicit empty-string skyflowId is rejected")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) XCTAssertNil(requestBody) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "element with column 'card_number'").description) @@ -402,7 +402,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { AdditionalFieldsRecord(tableName: "table1", data: ["column1": "value1"], skyflowId: "id1") ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) let update = requestBody?["update"] as! [String: Any] @@ -418,7 +418,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { AdditionalFieldsRecord(tableName: "table1", data: ["column1": "value1"], skyflowId: "") ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId is rejected")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNil(requestBody) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_SKYFLOW_ID(value: "additional fields record at index 0").description) @@ -432,7 +432,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { AdditionalFieldsRecord(tableName: "persons", data: ["email": "john@example.com"], skyflowId: "id1") ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Merge additionalFields sharing a skyflowId")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let update = requestBody?["update"] as! [String: Any] @@ -454,7 +454,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cardNumber!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) let update = requestBody?["update"] as! [String: Any] @@ -482,7 +482,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(updateElement!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed insert and update elements")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [insertElement!, updateElement!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let records = requestBody?["records"] as! [[String: Any]] @@ -512,7 +512,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element insert + additionalFields insert")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [insertElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let records = requestBody?["records"] as! [[String: Any]] @@ -541,7 +541,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element update + additionalFields insert")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let records = requestBody?["records"] as! [[String: Any]] @@ -556,8 +556,8 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { func testFullCombinationCollectUpdateUpsertAndAdditionalFields() { // The kitchen-sink scenario: an insert element, an update-by-skyflowId element, an // insert additionalFields record, and an update additionalFields record, all in one - // request - then upsert applied on top. Upsert only ever touches FlowVaultInsertRequestBody's - // "records" bucket (confirmed: FlowVaultUpdateRequestBody never reads options.upsert at + // request - then upsert applied on top. Upsert only ever touches CollectRequestBuilder.createInsertRequestBody's + // "records" bucket (confirmed: CollectRequestBuilder.createUpdateRequestBody never reads options.upsert at // all), so it should only decorate the matching insert record and leave "update" untouched. let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) @@ -579,7 +579,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Full combination")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [insertElement!, updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let records = requestBody?["records"] as! [[String: Any]] @@ -594,7 +594,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { // Apply upsert on top, matching only the "cards" table. let upsertOptions = [UpsertOption(tableName: "cards", uniqueColumns: ["card_number"], updateType: .UPDATE)] - let wireBody = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: requestBody!, options: FlowVaultICOptions(upsert: upsertOptions)) + let wireBody = CollectRequestBuilder.createInsertRequestBody(vaultID: "vault123", records: requestBody!, upsert: upsertOptions) let wireRecords = wireBody["records"] as! [[String: Any]] let cardsWireRecord = wireRecords.first { $0["tableName"] as? String == "cards" } @@ -603,15 +603,15 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let accountsWireRecord = wireRecords.first { $0["tableName"] as? String == "accounts" } XCTAssertNil(accountsWireRecord?["upsert"]) - // update bucket is a completely separate path (FlowVaultUpdateRequestBody) - upsert has - // no way to reach or affect it, confirmed structurally since FlowVaultInsertRequestBody + // update bucket is a completely separate path (CollectRequestBuilder.createUpdateRequestBody) - upsert has + // no way to reach or affect it, confirmed structurally since CollectRequestBuilder.createInsertRequestBody // only ever reads requestBody["records"]. XCTAssertEqual(update.count, 2) } // Not currently reachable from any documented flow (README only shows ElementValueMatchRule // used to allow a duplicate plain/insert element, not two update-by-skyflowId elements), but - // the code path exists in FlowVaultCollectRequestBody's update branch too - covering it now + // the code path exists in CollectRequestBuilder's update branch too - covering it now // in case a future update UI (e.g. confirm-password-style re-entry on an editable field) relies on it. func testCreateRequestBodyElementValueMatchRuleBypassesUpdateDuplicate() { let window = UIWindow() @@ -633,7 +633,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(updateElement2!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should not fail")) - let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement1!, updateElement2!], callback: callback, contextOptions: ContextOptions()) + let requestBody = CollectRequestBuilder.createCollectRecords(elements: [updateElement1!, updateElement2!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) let update = requestBody?["update"] as! [String: Any] diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift index cca5f177..9f3920cd 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift @@ -10,7 +10,7 @@ // import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index c0cef4c1..c5022c3a 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -11,7 +11,7 @@ import Foundation import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift index 7133fcb0..2a1fc8e7 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore class Skyflow_iOS_revealOptionsValidationTests: XCTestCase { diff --git a/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift b/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift index 64a125f9..71011630 100644 --- a/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift +++ b/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift @@ -5,7 +5,7 @@ // Minimal URLProtocol-based network mock. Register via URLProtocol.registerClass(MockURLProtocol.self) // before the request fires (URLSession(configuration: .default) consults globally registered // protocol classes), and unregister afterward. Used for tests that need to exercise real -// URLSession.dataTask call sites (e.g. FlowVaultInsertAPICallback's insert+update merge), where +// URLSession.dataTask call sites (e.g. FlowVaultCollectAPICallback's insert+update merge), where // bypassing the network layer entirely (as most other tests do via processResponse(data:response:error:)) // wouldn't exercise the merge logic itself. diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift index 12959649..7d127d9f 100644 --- a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift @@ -3,7 +3,7 @@ */ // Unit tests for the dormant v1 PDB CollectRequestBody (kept for potential future PDB reuse). -// Distinct from FlowVaultCollectRequestBody, which is the live class covering this same logic. +// Distinct from CollectRequestBuilder, which is the live class covering this same logic. import XCTest @testable import Skyflow diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift index f8b38e3c..41351d79 100644 --- a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift @@ -68,7 +68,7 @@ final class skyflow_iOS_legacyUtilTests: XCTestCase { } // Dormant v1 ICOptions.validateUpsert() (untyped upsert dicts), kept for potential future - // PDB reuse. Distinct from FlowVaultICOptions.validateUpsert(), which covers the live path. + // PDB reuse. Distinct from RequestValidators.checkUpsertOptions in FlowVault, which covers the live path. func testICOptionsValidateUpsertValid() { let callback = DemoAPICallback(expectation: XCTestExpectation()) diff --git a/Tests/skyflow-iOS-revealTests/DemoImpl.swift b/Tests/skyflow-iOS-revealTests/DemoImpl.swift index 7c40c321..530a2067 100644 --- a/Tests/skyflow-iOS-revealTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-revealTests/DemoImpl.swift @@ -4,7 +4,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class DemoTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift b/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift index 25c8ff14..ad2e8a04 100644 --- a/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift +++ b/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore diff --git a/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift b/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift index 5e1680c8..b169eae3 100644 --- a/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift +++ b/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore class RevealSkyflowLabelViewTests: XCTestCase { @@ -29,7 +29,7 @@ class RevealSkyflowLabelViewTests: XCTestCase { let config = Configuration(vaultID: "vault id", vaultURL:"vault url", tokenProvider: DemoTokenProvider(), options: Options(env: Env.DEV)) // Initialize skyflow client - let skyflowClient = SkyflowFlowVaultIOS.initialize(config) + let skyflowClient = SkyflowFlowVault.initialize(config) // Create a Reveal Container let container = skyflowClient.container(type: ContainerType.REVEAL) diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index f50668ee..752f6dca 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -5,7 +5,7 @@ import Foundation import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -110,7 +110,7 @@ class skyflow_iOS_revealTests: XCTestCase { let revealElementInput = getRevealElementInput() let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] + let requestBody = RevealRequestBuilder.createRevealRecords(elements: [revealElement!]) as! [String: [[String: Any]]] let result: [String: [[String: Any]]] = ["records": [["token": revealTestId]]] diff --git a/Tests/skyflow-iOS-utilTests/DemoImpl.swift b/Tests/skyflow-iOS-utilTests/DemoImpl.swift index 8de90a93..8762ee0c 100644 --- a/Tests/skyflow-iOS-utilTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-utilTests/DemoImpl.swift @@ -4,7 +4,7 @@ import Foundation import XCTest -import SkyflowFlowVaultIOS +import SkyflowFlowVault import SkyflowCore public class DemoTokenProvider: TokenProvider { diff --git a/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift index 64a125f9..71011630 100644 --- a/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift +++ b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift @@ -5,7 +5,7 @@ // Minimal URLProtocol-based network mock. Register via URLProtocol.registerClass(MockURLProtocol.self) // before the request fires (URLSession(configuration: .default) consults globally registered // protocol classes), and unregister afterward. Used for tests that need to exercise real -// URLSession.dataTask call sites (e.g. FlowVaultInsertAPICallback's insert+update merge), where +// URLSession.dataTask call sites (e.g. FlowVaultCollectAPICallback's insert+update merge), where // bypassing the network layer entirely (as most other tests do via processResponse(data:response:error:)) // wouldn't exercise the merge logic itself. diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index e8af5ae7..6f77e535 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -3,7 +3,7 @@ */ import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore @@ -15,7 +15,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { self.collectCallback = FlowVaultCollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions()) } @@ -79,7 +79,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { func testGetCollectResponseWithTokens() { let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] - self.collectCallback.options = FlowVaultICOptions() + self.collectCallback.upsert = nil do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) @@ -105,7 +105,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { "data": ["field": "value"], "hashedData": ["field": "hashed-value"] ]]] - self.collectCallback.options = FlowVaultICOptions() + self.collectCallback.upsert = nil do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) @@ -246,7 +246,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions() ) @@ -283,7 +283,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": insertRecords], - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions() ) @@ -320,7 +320,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": insertRecords], - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions() ) @@ -361,7 +361,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": insertRecords], - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions() ) @@ -409,7 +409,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { """ let responseData = json.data(using: .utf8)! let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - self.collectCallback.options = FlowVaultICOptions() + self.collectCallback.upsert = nil do { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) @@ -485,7 +485,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { """ let responseData = json.data(using: .utf8)! let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) - self.collectCallback.options = FlowVaultICOptions() + self.collectCallback.upsert = nil do { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) @@ -501,16 +501,16 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { // The tests above all call processResponse(data:response:error:) directly, which only // exercises parsing a single HTTP response body. When a request has BOTH inserts and - // updates, FlowVaultInsertAPICallback.onSuccess fires two REAL network calls (v2/records/insert + // updates, FlowVaultCollectAPICallback.onSuccess fires two REAL network calls (v2/records/insert // and v2/records/update) and merges both results via a DispatchGroup - that merge logic can // only be exercised by going through onSuccess with real URLSession dispatch, hence the // URLProtocol mock here. func testInsertAndUpdateResponsesMergeIntoOneCollectResponse() { let mockConfiguration = URLSessionConfiguration.ephemeral mockConfiguration.protocolClasses = [MockURLProtocol.self] - let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration - FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration - defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + let originalConfiguration = FlowVaultCollectAPICallback.urlSessionConfiguration + FlowVaultCollectAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultCollectAPICallback.urlSessionConfiguration = originalConfiguration } MockURLProtocol.requestHandler = { request in let url = request.url!.absoluteString @@ -536,11 +536,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] ] - let insertApiCallback = FlowVaultInsertAPICallback( + let insertApiCallback = FlowVaultCollectAPICallback( callback: callback.asCollectCallback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: records, - options: FlowVaultICOptions(), + upsert: nil, contextOptions: ContextOptions() ) // onSuccess's parameter here is the bearer token from TokenAPICallback - unused by this @@ -564,9 +564,9 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { // stringified dump of the internal {"records", "errors"} merge container. let mockConfiguration = URLSessionConfiguration.ephemeral mockConfiguration.protocolClasses = [MockURLProtocol.self] - let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration - FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration - defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + let originalConfiguration = FlowVaultCollectAPICallback.urlSessionConfiguration + FlowVaultCollectAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultCollectAPICallback.urlSessionConfiguration = originalConfiguration } MockURLProtocol.requestHandler = { request in let url = request.url!.absoluteString @@ -593,11 +593,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] ] - let insertApiCallback = FlowVaultInsertAPICallback( + let insertApiCallback = FlowVaultCollectAPICallback( callback: callback.asCollectCallback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: records, - options: FlowVaultICOptions(), + upsert: nil, contextOptions: ContextOptions() ) insertApiCallback.onSuccess("dummy-token") diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift index 6ba1fe78..f76f3c27 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift @@ -3,7 +3,7 @@ */ import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore final class skyflow_iOS_cvvMockTests: XCTestCase { @@ -81,7 +81,7 @@ final class skyflow_iOS_cvvMockTests: XCTestCase { } func testCaptureFirstElementWinsOnDuplicateColumn() { - // Mirrors FlowVaultCollectRequestBody's own dedup rule (e.g. ElementValueMatchRule): + // Mirrors CollectRequestBuilder's own dedup rule (e.g. ElementValueMatchRule): // when two elements target the same column, only the first one's value reaches the vault. let first = makeCVVElement(table: "persons", column: "cvv", value: "111") let second = makeCVVElement(table: "persons", column: "cvv", value: "222") @@ -362,7 +362,7 @@ final class skyflow_iOS_cvvMockTests: XCTestCase { // only reliably intercepts URLSession.shared, not ad-hoc .default sessions - confirmed by this // failing against the real network (github.com/.../example.org) rather than the mock handler. // Making that interceptable would require adding a test-only seam to production SDK code - // (e.g. FlowVaultInsertAPICallback's injectable urlSessionConfiguration), which is out of + // (e.g. FlowVaultCollectAPICallback's injectable urlSessionConfiguration), which is out of // scope here. The wiring itself (cvvMap capture + CVVMaskingCallback insertion in // CollectContainer.swift/ComposableContainer.swift) is two lines per container and is // exercised for real via the NormalTesting sample app's CVV Mock Scenarios screen against a diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index 3640d453..72f0d8ad 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -3,19 +3,19 @@ */ import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore final class skyflow_iOS_insertUtilTests: XCTestCase { - var collectCallback: FlowVaultInsertAPICallback! = nil + var collectCallback: FlowVaultCollectAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] override func setUp() { - self.collectCallback = FlowVaultInsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultCollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions()) } @@ -77,7 +77,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { func testGetCollectResponseWithTokens() { let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] - self.collectCallback.options = FlowVaultICOptions() + self.collectCallback.upsert = nil do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) @@ -103,7 +103,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { "data": ["field": "value"], "hashedData": ["field": "hashed-value"] ]]] - self.collectCallback.options = FlowVaultICOptions() + self.collectCallback.upsert = nil do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) @@ -235,11 +235,11 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { "fields": ["field": "value"] ] - let collectCallback = FlowVaultInsertAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions() ) @@ -272,11 +272,11 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { "fields": ["field": "value"] ] - let collectCallback = FlowVaultInsertAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: FlowVaultICOptions(additionalFields: nil), + upsert: nil, contextOptions: ContextOptions() ) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index 523220f3..0be3316b 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -4,7 +4,7 @@ // swiftlint:disable file_length import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore // swiftlint:disable:next type_body_length @@ -66,7 +66,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { func testConstructV2DetokenizeRequestBody() { self.revealApiCallback.records = [RevealRequestRecord(token: "token1"), RevealRequestRecord(token: "token2")] - let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: self.revealApiCallback.records) + let result = RevealRequestBuilder.createDetokenizeRequestBody(vaultID: "vault123", records: self.revealApiCallback.records) XCTAssertEqual(result["vaultID"] as! String, "vault123") XCTAssertEqual(result["tokens"] as! [String], ["token1", "token2"]) @@ -76,7 +76,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { func testConstructV2DetokenizeRequestBodyWithTokenGroupRedactions() { let records = [RevealRequestRecord(token: "token1")] let tokenGroupRedactions = [TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED")] - let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + let result = RevealRequestBuilder.createDetokenizeRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) let redactions = result["tokenGroupRedactions"] as! [[String: Any]] XCTAssertEqual(redactions.count, 1) @@ -211,7 +211,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED"), TokenGroupRedaction(tokenGroupName: "group1", redaction: "PLAIN_TEXT") ] - let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + let result = RevealRequestBuilder.createDetokenizeRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) let redactions = result["tokenGroupRedactions"] as! [[String: Any]] XCTAssertEqual(redactions.count, 2) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index 8f41579b..c82a555a 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -3,7 +3,7 @@ */ import XCTest -@testable import SkyflowFlowVaultIOS +@testable import SkyflowFlowVault @testable import SkyflowCore final class skyflow_iOS_utilTests: XCTestCase { @@ -63,7 +63,7 @@ final class skyflow_iOS_utilTests: XCTestCase { XCTAssertEqual(textField.secureText, "4111 1111 1111 1111") } func testConstructV2RequestBody() { - let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions()) + let result = CollectRequestBuilder.createInsertRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], upsert: nil) XCTAssertEqual(result["vaultID"] as! String, "vault123") let records = result["records"] as! [[String: Any]] XCTAssertEqual(records[0]["tableName"] as! String, "table") @@ -72,7 +72,7 @@ final class skyflow_iOS_utilTests: XCTestCase { func testConstructV2RequestBodyWithUpsert() { let upsert = [UpsertOption(tableName: "table", uniqueColumns: ["field1"], updateType: .REPLACE)] - let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let result = CollectRequestBuilder.createInsertRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], upsert: upsert) let records = result["records"] as! [[String: Any]] let upsertPayload = records[0]["upsert"] as! [String: Any] XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) @@ -84,7 +84,7 @@ final class skyflow_iOS_utilTests: XCTestCase { // "updateType" is omitted from the wire payload rather than serialized as null. func testConstructV2RequestBodyWithUpsertAndNilUpdateType() { let upsert = [UpsertOption(tableName: "table", uniqueColumns: ["field1"])] - let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let result = CollectRequestBuilder.createInsertRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], upsert: upsert) let records = result["records"] as! [[String: Any]] let upsertPayload = records[0]["upsert"] as! [String: Any] XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) @@ -92,13 +92,13 @@ final class skyflow_iOS_utilTests: XCTestCase { } - func testFlowVaultUpdateRequestBody() { + func testCreateUpdateRequestBody() { let records: [[String: Any]] = [ ["skyflowID": "id1", "tableName": "table1", "data": ["email": "a@b.com"]], ["skyflowID": "id2", "tableName": "table2", "data": ["name": "chiku"]] ] - let result = FlowVaultUpdateRequestBody.createRequestBody(vaultID: "vault123", records: records) + let result = CollectRequestBuilder.createUpdateRequestBody(vaultID: "vault123", records: records) XCTAssertEqual(result["vaultID"] as? String, "vault123") let resultRecords = result["records"] as! [[String: Any]] From e11afc9771869d9dfd7c258fed0f761aec29a1df Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 13 Aug 2026 09:52:25 +0530 Subject: [PATCH 03/16] SK-3043 refactor code for client --- Sources/Skyflow/client/Client.swift | 220 +++++++++- Sources/Skyflow/client/ClientOperations.swift | 377 ------------------ .../collect/CollectContainerOperations.swift | 55 +-- Sources/Skyflow/collect/ICOptions.swift | 36 -- .../ComposableContainerOperations.swift | 56 +-- .../reveal/RevealContainerOperations.swift | 21 +- .../client/{ClientBase.swift => Client.swift} | 13 +- .../SkyflowCore/containers/Container.swift | 6 +- .../models/BaseConfiguration.swift | 2 +- Sources/SkyflowFlowVault/client/Client.swift | 110 ++++- .../client/ClientOperations.swift | 116 ------ .../validations/RequestValidators.swift | 2 +- .../skyflow_iOS_validateGetRecordTest.swift | 56 +-- .../skyflow_iOS_legacyUtilTests.swift | 65 ++- 14 files changed, 405 insertions(+), 730 deletions(-) delete mode 100644 Sources/Skyflow/client/ClientOperations.swift rename Sources/SkyflowCore/client/{ClientBase.swift => Client.swift} (86%) delete mode 100644 Sources/SkyflowFlowVault/client/ClientOperations.swift diff --git a/Sources/Skyflow/client/Client.swift b/Sources/Skyflow/client/Client.swift index f9bf19c6..a4e4665f 100644 --- a/Sources/Skyflow/client/Client.swift +++ b/Sources/Skyflow/client/Client.swift @@ -3,18 +3,230 @@ */ // Skyflow (legacy v1) entry points on the shared SkyflowCore Client class. -// The v1 operations live in ClientOperations.swift as further extensions. +// The v1 operations live below as further extensions. // Future enhancements specific to this SDK go in extensions in this target // without affecting the FlowVault SDK. import Foundation -public extension Client { - convenience init(_ skyflowConfig: Configuration) { +extension Client { + public convenience init(_ skyflowConfig: Configuration) { self.init(skyflowConfig.data) } - func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { + public func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { return makeContainer(type: type, options: options?.data) } + + // MARK: - Legacy (v1) contract operations + + public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .INSERT + Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) + var errorCode: ErrorCodes? + + if records["records"] == nil { + errorCode = .RECORDS_KEY_ERROR() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) + if let recordEntries = records["records"] as? [[String: Any]] { + for (index, record) in recordEntries.enumerated() { + if record["table"] != nil { + if !(record["table"] is String) { + errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } else { + if (record["table"] as! String).isEmpty { + errorCode = .EMPTY_TABLE_NAME() + } else { + if record["fields"] != nil { + if !(record["fields"] is [String: Any]) { + errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") + break + } + let fields = record["fields"] as! [String: Any] + if fields.isEmpty { + errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") + } + } else { + errorCode = .FIELDS_KEY_ERROR(value: "\(index)") + } + } + } + } else { + errorCode = .TABLE_KEY_ERROR(value: "\(index)") + } + } + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } else { + if let upsert = options.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.post(records: records, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) + } + } else { + errorCode = .INVALID_RECORDS_TYPE() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + + public func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .DETOKENIZE + Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) + } + + if let tokens = records["records"] as? [[String: Any]] { + var list: [RevealRequestRecord] = [] + if tokens.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index,token) in tokens.enumerated() { + let errorCode = RequestValidators.checkDetokenizeRecord(token: token, index: index) + if errorCode == nil, let id = token["token"] as? String { + if token["redaction"] == nil{ + list.append(RevealRequestRecord(token: id, redaction: RedactionType.PLAIN_TEXT.rawValue)) + } else if let redaction = token["redaction"] as? RedactionType{ + list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) + } + } else { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + + public func getById(records: [String: Any], callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .GETBYID + Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_GET_BY_ID_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) //Check + } + + if let entries = records["records"] as? [[String: Any]] { + var list: [GetByIdRecord] = [] + if entries.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index, entry) in entries.enumerated() { + let errorCode = RequestValidators.checkGetByIdEntry(entry: entry, index: index) + if errorCode != nil { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } else { + if let ids = entry["ids"] as? [String], let table = entry["table"] as? String, let redaction = entry["redaction"] as? RedactionType { + list.append(GetByIdRecord(ids: ids, table: table, redaction: redaction.rawValue)) + } + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .GET_BY_ID_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.getById(records: list, callback: logCallback, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + public func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .GET + Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + if let entries = records["records"] as? [[String: Any]] { + var list: [GetRecord] = [] + if entries.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index,entry) in entries.enumerated() { + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: options, index: index) + if errorCode != nil { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } else { + if let ids = entry["ids"] as? [String], let table = entry["table"] as? String { + if let redaction = entry["redaction"] as? RedactionType { + list.append(GetRecord(ids: ids, table: table, redaction: redaction.rawValue)) + } else + { + list.append(GetRecord(ids: ids, table: table)) + } + + } + if let columnValues = entry["columnValues"] as? [String], let table = entry["table"] as? String, let columnName = entry["columnName"] as? String, let redaction = entry["redaction"] as? RedactionType { + list.append(GetRecord(columnValues: columnValues, table: table, columnName: columnName, redaction: redaction.rawValue)) + } + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, onSuccessHandler: { + Log.info(message: .GET_SUCCESS, contextOptions: tempContextOptions) + }, onFailureHandler: { + + }) + self.apiClient.getRecord(records: list, callback: logCallback, getOptions: options, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + + internal func createDetokenizeRecords(_ IDsToTokens: [String: String]) -> [String: [[String: String]]]{ + var records = [] as [[String : String]] + var index = 0 + for (_, token) in IDsToTokens { + records.append(["token": token]) + index += 1 + } + + return ["records": records] + } + } diff --git a/Sources/Skyflow/client/ClientOperations.swift b/Sources/Skyflow/client/ClientOperations.swift deleted file mode 100644 index 141ef6e5..00000000 --- a/Sources/Skyflow/client/ClientOperations.swift +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Legacy (v1) contract operations on this SDK's Client class (declared in Client.swift). - -import Foundation - -extension Client { - public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .INSERT - Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) - var errorCode: ErrorCodes? - - if records["records"] == nil { - errorCode = .RECORDS_KEY_ERROR() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) - if let recordEntries = records["records"] as? [[String: Any]] { - for (index, record) in recordEntries.enumerated() { - if record["table"] != nil { - if !(record["table"] is String) { - errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } else { - if (record["table"] as! String).isEmpty { - errorCode = .EMPTY_TABLE_NAME() - } else { - if record["fields"] != nil { - if !(record["fields"] is [String: Any]) { - errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") - break - } - let fields = record["fields"] as! [String: Any] - if fields.isEmpty { - errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") - } - } else { - errorCode = .FIELDS_KEY_ERROR(value: "\(index)") - } - } - } - } else { - errorCode = .TABLE_KEY_ERROR(value: "\(index)") - } - } - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } else { - if options.upsert != nil { - if icOptions.validateUpsert() { - return; - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.post(records: records, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - - public func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .DETOKENIZE - func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { - if token["redaction"] != nil { - guard let _ = token["redaction"] as? RedactionType else { - return .INVALID_REDACTION_TYPE() - } - } - if token["token"] == nil { - return .ID_KEY_ERROR() - } else { - guard let _ = token["token"] as? String else { - return .INVALID_TOKEN_TYPE(value: "\(index)") - } - } - return nil - } - - Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) - } - - if let tokens = records["records"] as? [[String: Any]] { - var list: [RevealRequestRecord] = [] - if tokens.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index,token) in tokens.enumerated() { - let errorCode = checkRecord(token: token, index: index) - if errorCode == nil, let id = token["token"] as? String { - if token["redaction"] == nil{ - list.append(RevealRequestRecord(token: id, redaction: RedactionType.PLAIN_TEXT.rawValue)) - } else if let redaction = token["redaction"] as? RedactionType{ - list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) - } - } else { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - - public func getById(records: [String: Any], callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .GETBYID - Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_GET_BY_ID_INPUT, contextOptions: tempContextOptions) - - func checkEntry(entry: [String: Any], index: Int) -> ErrorCodes? { - if entry.isEmpty { - return .EMPTY_RECORDS_OBJECT() - } - if entry["ids"] == nil { - return .MISSING_KEY_IDS(value: "\(index)") - } - if !(entry["ids"] is [String]) { - return .INVALID_IDS_TYPE() - } - if ((entry["ids"] as? [String])?.count == 0) { - return .EMPTY_IDS(value: "\(index)") - } - let ids = entry["ids"] as! [String] - for id in ids { - if (id == "") { - return .EMPTY_ID_VALUE(value: "\(index)") - } - } - if entry["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(entry["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if ((entry["table"] as? String) == "") { - return .EMPTY_TABLE_NAME() - } - if entry["redaction"] == nil { - return .REDACTION_KEY_ERROR(value: "\(index)") - } - if (entry["redaction"] as? RedactionType) != nil { - return nil - } else { - return .INVALID_REDACTION_TYPE() - } - } - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) //Check - } - - if let entries = records["records"] as? [[String: Any]] { - var list: [GetByIdRecord] = [] - if entries.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index, entry) in entries.enumerated() { - let errorCode = checkEntry(entry: entry, index: index) - if errorCode != nil { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } else { - if let ids = entry["ids"] as? [String], let table = entry["table"] as? String, let redaction = entry["redaction"] as? RedactionType { - list.append(GetByIdRecord(ids: ids, table: table, redaction: redaction.rawValue)) - } - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .GET_BY_ID_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.getById(records: list, callback: logCallback, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - public func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .GET - Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) - if self.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - if let entries = records["records"] as? [[String: Any]] { - var list: [GetRecord] = [] - if entries.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index,entry) in entries.enumerated() { - let errorCode = validateGetRecords(entry: entry, getOptions: options, index: index) - if errorCode != nil { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } else { - if let ids = entry["ids"] as? [String], let table = entry["table"] as? String { - if let redaction = entry["redaction"] as? RedactionType { - list.append(GetRecord(ids: ids, table: table, redaction: redaction.rawValue)) - } else - { - list.append(GetRecord(ids: ids, table: table)) - } - - } - if let columnValues = entry["columnValues"] as? [String], let table = entry["table"] as? String, let columnName = entry["columnName"] as? String, let redaction = entry["redaction"] as? RedactionType { - list.append(GetRecord(columnValues: columnValues, table: table, columnName: columnName, redaction: redaction.rawValue)) - } - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, onSuccessHandler: { - Log.info(message: .GET_SUCCESS, contextOptions: tempContextOptions) - }, onFailureHandler: { - - }) - self.apiClient.getRecord(records: list, callback: logCallback, getOptions: options, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - - internal func createDetokenizeRecords(_ IDsToTokens: [String: String]) -> [String: [[String: String]]]{ - var records = [] as [[String : String]] - var index = 0 - for (_, token) in IDsToTokens { - records.append(["token": token]) - index += 1 - } - - return ["records": records] - } -} - -// Validation for the v1 get/getById records dictionaries (moved from core: -// only the legacy contract exposes these operations). -extension Client { - package func validateGetRecords(entry: [String: Any], getOptions: GetOptions, index: Int) -> ErrorCodes? { - if entry.isEmpty { - return .EMPTY_RECORDS_OBJECT() - } - if (entry["ids"] != nil ){ - if !(entry["ids"] is [String]) { - return .INVALID_IDS_TYPE() - } - if ((entry["ids"] as? [String])?.count == 0) { - return .EMPTY_IDS(value: "\(index)") - } - let ids = entry["ids"] as! [String] - for id in ids { - if (id == "") { - return .EMPTY_ID_VALUE(value: "\(index)") - } - } - } - if entry["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(entry["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if ((entry["table"] as? String) == "") { - return .EMPTY_TABLE_NAME() - } - - if( getOptions.tokens == true ){ - if (entry["columnName"] != nil || entry["columnValues"] != nil){ - return .TOKENS_GET_COLUMN_NOT_SUPPPORTED() - - } - if (entry["redaction"] as? RedactionType) != nil { - return .REDACTION_WITH_TOKEN_NOT_SUPPORTED() - } - } else { - if entry["redaction"] == nil { - return .REDACTION_KEY_ERROR(value: "\(index)") - } else if (entry["redaction"] as? RedactionType) == nil { - return .INVALID_REDACTION_TYPE() - } - } - - if(entry["columnName"] == nil){ - if ((entry["ids"] == nil) && (entry["columnValues"] == nil)){ - return .MISSING_IDS_OR_COLUMN_VALUES_IN_GET() - } - } else if (entry["columnName"] != nil && entry["columnValues"] == nil){ - return .MISSING_RECORD_COLUMN_VALUE() - } else if !(entry["columnName"] is String){ - return .INVALID_COLUMN_NAME(value: "\(index)") - } else if ((entry["ids"] != nil) && (entry["columnName"] != nil)){ - return .SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED() - } - if (entry["columnValues"] != nil){ - if ((entry["columnValues"] as? [String])?.count == 0) { - return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") - } - if !(entry["columnValues"] is [String]) { - return .INVALID_COLUMN_VALUES_IN_GET() - } - if ((entry["columnValues"] as? [String])?.count == 0) { - return .EMPTY_RECORD_COLUMN_VALUES(value: "\(index)") - } - let columnValues = entry["columnValues"] as! [String] - for columnValue in columnValues { - if (columnValue == "") { - return .EMPTY_COLUMN_VALUE(value: "\(index)") - } - } - if( entry["columnName"] == nil ){ - return .MISSING_COLUMN_NAME() - } else if((entry["columnName"] as? String) == ""){ - return .EMPTY_COLUMN_NAME() - } - } - - return nil - } -} diff --git a/Sources/Skyflow/collect/CollectContainerOperations.swift b/Sources/Skyflow/collect/CollectContainerOperations.swift index 1a122452..07c313c7 100644 --- a/Sources/Skyflow/collect/CollectContainerOperations.swift +++ b/Sources/Skyflow/collect/CollectContainerOperations.swift @@ -11,12 +11,7 @@ public extension Container { func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -24,7 +19,7 @@ public extension Container { Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = checkElement(element: element) + errorCode = RequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return @@ -59,7 +54,7 @@ public extension Container { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkRecord(record: record, index: index) + errorCode = RequestValidators.checkRecord(record: record, index: index) if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } @@ -72,9 +67,9 @@ public extension Container { } let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) - if options?.upsert != nil { - if icOptions.validateUpsert() { - return; + if let upsert = options?.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) } } if records != nil { @@ -88,44 +83,6 @@ public extension Container { self.skyflow.apiClient.postAndUpdate(records: records!, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) } } - - private func checkElement(element: TextField) -> ErrorCodes? { - if element.collectInput.tableName.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) - } - - return nil - } - - private func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { - return .EMPTY_TABLE_NAME() - } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ - return .EMPTY_FIELDS_KEY(value: "\(index)") - } - - return nil - } } public extension Container { diff --git a/Sources/Skyflow/collect/ICOptions.swift b/Sources/Skyflow/collect/ICOptions.swift index 1b2562df..c2f0592d 100644 --- a/Sources/Skyflow/collect/ICOptions.swift +++ b/Sources/Skyflow/collect/ICOptions.swift @@ -25,40 +25,4 @@ internal struct ICOptions { self.callback = callback self.contextOptions = contextOptions } - - public func validateUpsert() -> Bool{ - if self.upsert != nil { - if self.upsert!.count == 0 { - let errorCode = ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY() - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - - for (index, currUpsertOption) in (self.upsert ?? [[:]]).enumerated() { - if currUpsertOption["table"] == nil { - let errorCode = ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "\(index)") - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - if currUpsertOption["column"] == nil { - let errorCode = ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "\(index)") - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - - if currUpsertOption["table"] as! String == "" { - let errorCode = ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - if currUpsertOption["column"] as! String == "" { - let errorCode = ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") - self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) - return true - } - } - - } - return false; - } } diff --git a/Sources/Skyflow/composable/ComposableContainerOperations.swift b/Sources/Skyflow/composable/ComposableContainerOperations.swift index 19ec78fb..891d7c4b 100644 --- a/Sources/Skyflow/composable/ComposableContainerOperations.swift +++ b/Sources/Skyflow/composable/ComposableContainerOperations.swift @@ -12,12 +12,7 @@ public extension Container { func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -25,7 +20,7 @@ public extension Container { Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = checkComposableElement(element: element) + errorCode = RequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return @@ -62,7 +57,7 @@ public extension Container { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkComposableRecord(record: record, index: index) + errorCode = RequestValidators.checkRecord(record: record, index: index) if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } @@ -75,9 +70,9 @@ public extension Container { } let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) - if options?.upsert != nil { - if icOptions.validateUpsert() { - return; + if let upsert = options?.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) } } if records != nil { @@ -91,45 +86,6 @@ public extension Container { self.skyflow.apiClient.postAndUpdate(records: records!, callback: logCallback, options: icOptions, contextOptions: tempContextOptions) } } - - private func checkComposableElement(element: TextField) -> ErrorCodes? { - if element.collectInput.tableName.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) - } - - return nil - } - - private func checkComposableRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { - return .EMPTY_TABLE_NAME() - } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ - return .EMPTY_FIELDS_KEY(value: "\(index)") - } - - return nil - } - } public extension Container { diff --git a/Sources/Skyflow/reveal/RevealContainerOperations.swift b/Sources/Skyflow/reveal/RevealContainerOperations.swift index 2a97a380..c46a2f30 100644 --- a/Sources/Skyflow/reveal/RevealContainerOperations.swift +++ b/Sources/Skyflow/reveal/RevealContainerOperations.swift @@ -10,12 +10,7 @@ public extension Container { func reveal(callback: Callback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER - if self.skyflow.vaultID.isEmpty { - let errorCode = ErrorCodes.EMPTY_VAULT_ID() - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - if self.skyflow.vaultURL == "/" { - let errorCode = ErrorCodes.EMPTY_VAULT_URL() + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errorCode: ErrorCodes? @@ -25,17 +20,9 @@ public extension Container { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return } - for element in self.revealElements { - if element.errorTriggered { - errorCode = .ERROR_TRIGGERED(value: element.triggeredErrorMessage) - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - if element.getToken().isEmpty { - errorCode = .EMPTY_TOKEN_ID() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } + if let elementError = RequestValidators.checkRevealElements(elements: self.revealElements) { + callback.onFailure(elementError.getErrorObject(contextOptions: tempContextOptions)) + return } let revealValueCallback = RevealValueCallback(callback: callback, revealElements: self.revealElements, contextOptions: tempContextOptions) let records = RevealRequestBody.createRequestBody(elements: self.revealElements) diff --git a/Sources/SkyflowCore/client/ClientBase.swift b/Sources/SkyflowCore/client/Client.swift similarity index 86% rename from Sources/SkyflowCore/client/ClientBase.swift rename to Sources/SkyflowCore/client/Client.swift index 62d85380..0fe6c28c 100644 --- a/Sources/SkyflowCore/client/ClientBase.swift +++ b/Sources/SkyflowCore/client/Client.swift @@ -2,15 +2,16 @@ * Copyright (c) 2022 Skyflow */ -// Shared base for the Skyflow client. Each SDK product defines its own public -// `Client` subclass where that contract's operations (insert/detokenize/get/ -// getById, ...) and any future SDK-specific enhancements live; the base holds -// shared state, container creation, and validation helpers. Applications can -// see the base type name but cannot construct, subclass, or use it (package init/members). +// The one shared Client class, used by both SDK products. It holds shared +// state, container creation, and validation helpers. Each SDK adds its own +// public convenience init (taking that SDK's Configuration) and its contract's +// operations (insert/detokenize/get/getById, ...) via extensions in its own +// target. Applications construct it only through an SDK's convenience init; +// the designated init and all members are package-visible only. import Foundation -open class ClientBase { +public class Client { package var vaultID: String package var apiClient: APIClient // Normalized base vault URL (always ends with "/"). Empty configuration input diff --git a/Sources/SkyflowCore/containers/Container.swift b/Sources/SkyflowCore/containers/Container.swift index c0302169..8ff1e6a3 100644 --- a/Sources/SkyflowCore/containers/Container.swift +++ b/Sources/SkyflowCore/containers/Container.swift @@ -12,15 +12,15 @@ import Foundation public class Container { - package var skyflow: ClientBase + package var skyflow: Client package var elements: [TextField] = [] package var revealElements: [Label] = [] package var containerOptions: BaseContainerOptions? = nil - package init(skyflow: ClientBase) { + package init(skyflow: Client) { self.skyflow = skyflow } - package init(skyflow: ClientBase, options: BaseContainerOptions? = nil){ + package init(skyflow: Client, options: BaseContainerOptions? = nil){ self.containerOptions = options self.skyflow = skyflow } diff --git a/Sources/SkyflowCore/models/BaseConfiguration.swift b/Sources/SkyflowCore/models/BaseConfiguration.swift index 97f5f200..fc295b75 100644 --- a/Sources/SkyflowCore/models/BaseConfiguration.swift +++ b/Sources/SkyflowCore/models/BaseConfiguration.swift @@ -4,7 +4,7 @@ // Shared storage for the client configuration. Each SDK product defines its // own public `BaseConfiguration` struct exposing that contract's initializer -// (identical in both contracts today); ClientBase consumes this value once at +// (identical in both contracts today); Client consumes this value once at // initialization and does not retain it. import Foundation diff --git a/Sources/SkyflowFlowVault/client/Client.swift b/Sources/SkyflowFlowVault/client/Client.swift index ab601429..29f15887 100644 --- a/Sources/SkyflowFlowVault/client/Client.swift +++ b/Sources/SkyflowFlowVault/client/Client.swift @@ -3,7 +3,7 @@ */ // FlowVault (v2) entry points on the shared SkyflowCore Client class. -// The v2 operations live in ClientOperations.swift as further extensions. +// The v2 operations live below as further extensions. // Future enhancements specific to this SDK go in extensions in this target // without affecting the legacy Skyflow SDK. @@ -17,4 +17,112 @@ public extension Client { func container(type: T.Type, options: ContainerOptions? = nil) -> Container? { return makeContainer(type: type, options: options?.data) } + + // MARK: - FlowVault (v2) contract operations + internal func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .INSERT + Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + var errorCode: ErrorCodes? + + if records["records"] == nil { + errorCode = .RECORDS_KEY_ERROR() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } + + Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) + if let recordEntries = records["records"] as? [[String: Any]] { + for (index, record) in recordEntries.enumerated() { + if record["table"] != nil { + if !(record["table"] is String) { + errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } else { + if (record["table"] as! String).isEmpty { + errorCode = .EMPTY_TABLE_NAME() + } else { + if record["fields"] != nil { + if !(record["fields"] is [String: Any]) { + errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") + break + } + let fields = record["fields"] as! [String: Any] + if fields.isEmpty { + errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") + } + } else { + errorCode = .FIELDS_KEY_ERROR(value: "\(index)") + } + } + } + } else { + errorCode = .TABLE_KEY_ERROR(value: "\(index)") + } + } + if errorCode != nil { + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + return + } else { + if let upsert = options.upsert { + if let upsertError = RequestValidators.checkUpsertOptions(upsert) { + return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.post(records: records, callback: logCallback, upsert: options.upsert, contextOptions: tempContextOptions) + } + } else { + errorCode = .INVALID_RECORDS_TYPE() + callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + + internal func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { + var tempContextOptions = self.contextOptions + tempContextOptions.interface = .DETOKENIZE + Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) + if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) + } + Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) + + if records["records"] == nil { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) + } + + if let tokens = records["records"] as? [[String: Any]] { + var list: [RevealRequestRecord] = [] + if tokens.isEmpty { + return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) + } + for (index,token) in tokens.enumerated() { + let errorCode = RequestValidators.checkDetokenizeRecord(token: token, index: index) + if errorCode == nil, let id = token["token"] as? String { + list.append(RevealRequestRecord(token: id)) + } else { + return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) + } + } + let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, + onSuccessHandler: { + Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) + }, + onFailureHandler: { + } + ) + self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) + } else { + callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) + } + } + } diff --git a/Sources/SkyflowFlowVault/client/ClientOperations.swift b/Sources/SkyflowFlowVault/client/ClientOperations.swift deleted file mode 100644 index 612f5264..00000000 --- a/Sources/SkyflowFlowVault/client/ClientOperations.swift +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// FlowVault (v2) contract operations on this SDK's Client class (declared in Client.swift). - -import Foundation - -extension Client { - internal func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .INSERT - Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { - return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - var errorCode: ErrorCodes? - - if records["records"] == nil { - errorCode = .RECORDS_KEY_ERROR() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) - if let recordEntries = records["records"] as? [[String: Any]] { - for (index, record) in recordEntries.enumerated() { - if record["table"] != nil { - if !(record["table"] is String) { - errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } else { - if (record["table"] as! String).isEmpty { - errorCode = .EMPTY_TABLE_NAME() - } else { - if record["fields"] != nil { - if !(record["fields"] is [String: Any]) { - errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") - break - } - let fields = record["fields"] as! [String: Any] - if fields.isEmpty { - errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") - } - } else { - errorCode = .FIELDS_KEY_ERROR(value: "\(index)") - } - } - } - } else { - errorCode = .TABLE_KEY_ERROR(value: "\(index)") - } - } - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } else { - if let upsert = options.upsert { - if let upsertError = RequestValidators.checkUpsertOptions(upsert) { - return callback.onFailure(upsertError.getErrorObject(contextOptions: tempContextOptions)) - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .INSERT_DATA_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.post(records: records, callback: logCallback, upsert: options.upsert, contextOptions: tempContextOptions) - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - - internal func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { - var tempContextOptions = self.contextOptions - tempContextOptions.interface = .DETOKENIZE - Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { - return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) - } - Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) - - if records["records"] == nil { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.RECORDS_KEY_ERROR().getErrorObject(contextOptions: tempContextOptions)) - } - - if let tokens = records["records"] as? [[String: Any]] { - var list: [RevealRequestRecord] = [] - if tokens.isEmpty { - return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) - } - for (index,token) in tokens.enumerated() { - let errorCode = RequestValidators.checkDetokenizeRecord(token: token, index: index) - if errorCode == nil, let id = token["token"] as? String { - list.append(RevealRequestRecord(token: id)) - } else { - return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, - onSuccessHandler: { - Log.info(message: .DETOKENIZE_SUCCESS, contextOptions: tempContextOptions) - }, - onFailureHandler: { - } - ) - self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) - } else { - callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) - } - } - -} diff --git a/Sources/SkyflowFlowVault/validations/RequestValidators.swift b/Sources/SkyflowFlowVault/validations/RequestValidators.swift index 93abadf5..d7cbaf3d 100644 --- a/Sources/SkyflowFlowVault/validations/RequestValidators.swift +++ b/Sources/SkyflowFlowVault/validations/RequestValidators.swift @@ -10,7 +10,7 @@ import Foundation internal class RequestValidators { // Guards shared by every operation: the client must have been configured - // with a vault ID and URL. (An empty vaultURL becomes "/" in ClientBase.) + // with a vault ID and URL. (An empty vaultURL becomes "/" in SkyflowCore.Client.) internal static func checkClientConfig(vaultID: String, vaultURL: String) -> ErrorCodes? { if vaultID.isEmpty { return .EMPTY_VAULT_ID() diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift b/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift index 1f887851..48fb4bf6 100644 --- a/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift +++ b/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift @@ -26,136 +26,136 @@ class skyflow_iOS_ValidateGetRecordTest: XCTestCase { } func testEmptyRecords(){ - let errorCode: ErrorCodes! = self.skyflow.validateGetRecords(entry:[:], getOptions: GetOptions(), index: 0) + let errorCode: ErrorCodes! = RequestValidators.validateGetRecords(entry:[:], getOptions: GetOptions(), index: 0) print(errorCode!) XCTAssertEqual(errorCode.description, ErrorCodes.EMPTY_RECORDS_OBJECT().description) } func testIdRecordEmptyRecordsObject(){ - let errorCode = self.skyflow.validateGetRecords(entry:["id": [], "table": "table1", "redaction": RedactionType.REDACTED], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry:["id": [], "table": "table1", "redaction": RedactionType.REDACTED], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.MISSING_IDS_OR_COLUMN_VALUES_IN_GET().description) } func testIdRecordEmptyRecordsObject2(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": "demo"], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": "demo"], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.INVALID_IDS_TYPE().description) } func testIdRecordEmptyRecordsObject3(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": [[:]]], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": [[:]]], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.INVALID_IDS_TYPE().description) } func testIdRecordEmptyRecordsInvalid(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": [12]], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": [12]], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.INVALID_IDS_TYPE().description) } func testIdRecordEmptyRecordsInvalid2(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": ["123", 12]], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": ["123", 12]], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.INVALID_IDS_TYPE().description) } func testIdRecordEmptyRecordsInvalid3(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": []], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": []], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.EMPTY_IDS(value: "\(0)").description) } func testIdRecordEmptyRecordsInvalid4(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": [""]], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": [""]], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.EMPTY_ID_VALUE(value: "\(0)").description) } func testInvalidRecordTableMissing(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": ["123"]], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": ["123"]], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.TABLE_KEY_ERROR(value: "\(0)").description) } func testIdRecordEmptyRecordsInvalidTable(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": ["123"], "table": []], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": ["123"], "table": []], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "\(0)").description) } func testIdRecordEmptyRecordsInvalid6(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": ["123"], "table": "table1"], getOptions: GetOptions(), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": ["123"], "table": "table1"], getOptions: GetOptions(), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.REDACTION_KEY_ERROR(value: "\(0)").description) } func testIdRecordEmptyRecordsInvalidTokenFalse(){ - let errorCode = self.skyflow.validateGetRecords(entry: ["ids": ["123"], "table": "table1"], getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: ["ids": ["123"], "table": "table1"], getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.REDACTION_KEY_ERROR(value: "\(0)").description) } func testIdRecordEmptyRecordsInvalidRedaction(){ let entry = ["ids": ["123"], "table": "table1", "redaction": []] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.INVALID_REDACTION_TYPE().description) } func testIdRecordEmptyRecordsValidRedaction(){ let entry = ["ids": ["123"], "table": "table1", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertNil(errorCode) } func testIdRecordEmptyRecordsValidRedactionGetOptionTrue(){ let entry = ["ids": ["123"], "table": "table1", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.REDACTION_WITH_TOKEN_NOT_SUPPORTED().description) } func testIdRecordEmptyRecordsNoRedactionGetOptionTrue(){ let entry = ["ids": ["123"], "table": "table1", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.REDACTION_WITH_TOKEN_NOT_SUPPORTED().description) } func testColumnRecordEmptyRecordsNoRedactionGetOptionTrue(){ let entry = ["table": "demo"] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.MISSING_IDS_OR_COLUMN_VALUES_IN_GET().description) } func testColumnRecordInvalidcolumnValues(){ let entry = ["columnValues": "", "table": "demo","redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.INVALID_COLUMN_VALUES_IN_GET().description) } func testColumnRecordInvalidEmptycolumnValues(){ let entry = ["columnValues": [], "table": "demo", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.EMPTY_RECORD_COLUMN_VALUES(value: "\(0)").description) } func testColumnRecordInvalidcolumnValuesArrayValues(){ let entry = ["columnValues": [[]], "table": "demo", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.INVALID_COLUMN_VALUES_IN_GET().description) } func testColumnRecordEmptycolumnValueArrayValues(){ let entry = ["columnValues": ["1234", ""], "table": "demo", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.EMPTY_COLUMN_VALUE(value: "\(0)").description) } func testColumnRecordInvalidcolumnNameArrayValues(){ let entry = ["columnValues": ["1234"], "table": "demo", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.MISSING_COLUMN_NAME().description) } func testColumnRecordInvalidcolumnNameValues(){ let entry = ["columnValues": ["1234"], "table": "demo", "columnName": [], "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.INVALID_COLUMN_NAME(value: "\(0)").description) } func testColumnRecordEmptycolumnNameValuesOptionTrue(){ let entry = ["columnValues": ["1234"], "table": "demo", "columnName": "", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.EMPTY_COLUMN_NAME().description) } func testColumnRecordValidcolumnNameValuesOptionTrue(){ let entry = ["columnValues": ["1234"], "table": "demo", "columnName": "123", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertNil(errorCode) } func testColumnRecordValidcolumnNameValuesOptionTrueWithId(){ let entry = ["columnValues": ["1234"], "table": "demo", "columnName": "123", "ids": ["123"], "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode!.description, ErrorCodes.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED().description) } func testColumnRecordValidcolumnValuesValuesOptionTrueWithId(){ let entry = ["columnValues": ["1234"], "table": "demo", "ids": ["123"], "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: false), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.MISSING_COLUMN_NAME().description) } func testColumnRecordValidcolumnValuesValuesOptionTrue2(){ let entry = ["columnValues": ["1234"], "table": "demo", "redaction": RedactionType.REDACTED] as [String : Any] - let errorCode = self.skyflow.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) + let errorCode = RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(tokens: true), index: 0) XCTAssertEqual(errorCode?.description, ErrorCodes.TOKENS_GET_COLUMN_NOT_SUPPPORTED().description) } diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift index 41351d79..08a527a9 100644 --- a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift +++ b/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift @@ -67,68 +67,51 @@ final class skyflow_iOS_legacyUtilTests: XCTestCase { XCTAssertNil(postPayload[0]["upsert"]) } - // Dormant v1 ICOptions.validateUpsert() (untyped upsert dicts), kept for potential future - // PDB reuse. Distinct from RequestValidators.checkUpsertOptions in FlowVault, which covers the live path. + // v1 upsert validation (untyped upsert dicts), now in RequestValidators like the + // FlowVault SDK's checkUpsertOptions; the operations deliver the returned error. - func testICOptionsValidateUpsertValid() { - let callback = DemoAPICallback(expectation: XCTestExpectation()) + func testCheckUpsertOptionsValid() { let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] - let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) - XCTAssertFalse(options.validateUpsert()) + XCTAssertNil(RequestValidators.checkUpsertOptions(upsert)) } - func testICOptionsValidateUpsertEmptyArray() { - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - let options = ICOptions(tokens: false, upsert: [], callback: callback, contextOptions: ContextOptions()) + func testCheckUpsertOptionsEmptyArray() { + let errorCode = RequestValidators.checkUpsertOptions([]) - XCTAssertTrue(options.validateUpsert()) - wait(for: [expectation], timeout: 10.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions()).localizedDescription) + XCTAssertEqual(errorCode?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions()).localizedDescription) } - func testICOptionsValidateUpsertMissingTableKey() { - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) + func testCheckUpsertOptionsMissingTableKey() { let upsert: [[String: Any]] = [["column": "email"]] - let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + let errorCode = RequestValidators.checkUpsertOptions(upsert) - XCTAssertTrue(options.validateUpsert()) - wait(for: [expectation], timeout: 10.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + XCTAssertEqual(errorCode?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) } - func testICOptionsValidateUpsertMissingColumnKey() { - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) + func testCheckUpsertOptionsMissingColumnKey() { let upsert: [[String: Any]] = [["table": "table1"]] - let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + let errorCode = RequestValidators.checkUpsertOptions(upsert) - XCTAssertTrue(options.validateUpsert()) - wait(for: [expectation], timeout: 10.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + XCTAssertEqual(errorCode?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) } - func testICOptionsValidateUpsertEmptyTableValue() { - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) + func testCheckUpsertOptionsEmptyTableValue() { let upsert: [[String: Any]] = [["table": "", "column": "email"]] - let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + let errorCode = RequestValidators.checkUpsertOptions(upsert) - XCTAssertTrue(options.validateUpsert()) - wait(for: [expectation], timeout: 10.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + XCTAssertEqual(errorCode?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) } - func testICOptionsValidateUpsertEmptyColumnValue() { - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) + func testCheckUpsertOptionsEmptyColumnValue() { let upsert: [[String: Any]] = [["table": "table1", "column": ""]] - let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + let errorCode = RequestValidators.checkUpsertOptions(upsert) - XCTAssertTrue(options.validateUpsert()) - wait(for: [expectation], timeout: 10.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + XCTAssertEqual(errorCode?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) } } From 27bcafaaf51f2dd698efbb3417f0ba7fa8163fce Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 14 Aug 2026 09:57:59 +0530 Subject: [PATCH 04/16] SK-3043 move common validators --- .github/workflows/beta-release.yml | 4 +- .github/workflows/bump_version.yml | 2 +- .github/workflows/internal_release.yml | 6 +-- .github/workflows/release.yml | 4 +- Package.swift | 11 ++-- Samples/README.md | 2 +- ...lt-ios.podspec => SkyflowFlowVault.podspec | 10 ++-- Sources/Skyflow/client/Client.swift | 9 ++-- .../collect/CollectContainerOperations.swift | 4 +- .../ComposableContainerOperations.swift | 4 +- .../reveal/RevealContainerOperations.swift | 4 +- .../validations/RequestValidators.swift | 40 +-------------- .../composable/ComposableContainer.swift | 4 +- .../validations/CoreRequestValidators.swift | 51 +++++++++++++++++++ Sources/SkyflowFlowVault/client/Client.swift | 4 +- .../collect/CollectCallback.swift | 33 ++++++++++++ .../collect/CollectContainerOperations.swift | 13 ++--- .../ComposableContainerOperations.swift | 4 +- .../models/CollectResponse.swift | 27 ---------- .../models/RevealResponse.swift | 30 ----------- .../reveal/RevealCallback.swift | 36 +++++++++++++ .../reveal/RevealContainerOperations.swift | 14 ++--- .../validations/RequestValidators.swift | 40 +-------------- scripts/bump_version.sh | 4 +- 24 files changed, 168 insertions(+), 192 deletions(-) rename Skyflow-flowvault-ios.podspec => SkyflowFlowVault.podspec (83%) create mode 100644 Sources/SkyflowCore/validations/CoreRequestValidators.swift create mode 100644 Sources/SkyflowFlowVault/collect/CollectCallback.swift create mode 100644 Sources/SkyflowFlowVault/reveal/RevealCallback.swift diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index cbb4527e..99031e6d 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -25,8 +25,8 @@ jobs: run: | set -eo pipefail pod lib lint Skyflow.podspec --allow-warnings - pod lib lint Skyflow-flowvault-ios.podspec --allow-warnings + pod lib lint SkyflowFlowVault.podspec --allow-warnings pod trunk push Skyflow.podspec --allow-warnings - pod trunk push Skyflow-flowvault-ios.podspec --allow-warnings + pod trunk push SkyflowFlowVault.podspec --allow-warnings env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index d56507d8..4058a573 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -41,7 +41,7 @@ jobs: run: | git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com - git add Skyflow.podspec Skyflow-flowvault-ios.podspec + git add Skyflow.podspec SkyflowFlowVault.podspec git add Sources/SkyflowCore/Version.swift git commit -m "[AUTOMATED] Public Release ${{ steps.version.outputs.version }}" git push origin \ No newline at end of file diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index 97678fe0..fe351a81 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -7,7 +7,7 @@ on: - '*' paths-ignore: - 'Skyflow.podspec' - - 'Skyflow-flowvault-ios.podspec' + - 'SkyflowFlowVault.podspec' - '*.md' - 'Sources/SkyflowCore/Version.swift' branches: @@ -37,7 +37,7 @@ jobs: run: | git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com - git add Skyflow.podspec Skyflow-flowvault-ios.podspec + git add Skyflow.podspec SkyflowFlowVault.podspec git add Sources/SkyflowCore/Version.swift git commit -m "[AUTOMATED] Private Release ${{ steps.previoustag.outputs.tag }}.$(git rev-parse --short $GITHUB_SHA)" git push origin @@ -54,5 +54,5 @@ jobs: run: | pod repo add private_spec git@github.com:skyflowapi/skyflow-iOS-private-spec.git pod repo push private_spec ./Skyflow.podspec --allow-warnings - pod repo push private_spec ./Skyflow-flowvault-ios.podspec --allow-warnings + pod repo push private_spec ./SkyflowFlowVault.podspec --allow-warnings echo "Published as Private pod" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5855abf6..f931eda3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,8 +27,8 @@ jobs: run: | set -eo pipefail pod lib lint Skyflow.podspec --allow-warnings - pod lib lint Skyflow-flowvault-ios.podspec --allow-warnings + pod lib lint SkyflowFlowVault.podspec --allow-warnings pod trunk push Skyflow.podspec --allow-warnings - pod trunk push Skyflow-flowvault-ios.podspec --allow-warnings + pod trunk push SkyflowFlowVault.podspec --allow-warnings env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/Package.swift b/Package.swift index d2f09caa..f9497ab4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,3 @@ -// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -9,22 +8,20 @@ let package = Package( .iOS(.v13) ], products: [ - // Legacy vault SDK 1(v1 API contract). + // PDB vault SDK 1(v1 API contract). .library( name: "Skyflow", targets: ["Skyflow"]), - // FlowVault SDK (v2 API contract). Module name is SkyflowFlowVault. + // FlowVault SDK (v2 API contract). .library( - name: "Skyflow-flowvault-ios", - targets: ["SkyflowFlowVault"]), // check this + name: "SkyflowFlowVault", + targets: ["SkyflowFlowVault"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ - // Shared, contract-agnostic core: UI elements, validations, styles, - // container machinery, token/JWT handling, logging, errors, utils. .target( name: "SkyflowCore", dependencies: [], diff --git a/Samples/README.md b/Samples/README.md index 933044f2..bb2588eb 100644 --- a/Samples/README.md +++ b/Samples/README.md @@ -12,7 +12,7 @@ Sample apps are organized by SDK — pick the folder for the product you use. | [UpsertFeature](Skyflow/UpsertFeature/) | Insert with upsert options | | [Validations](Skyflow/Validations/) | Built-in and custom validation rules | -## [`SkyflowFlowVault/`](SkyflowFlowVault/) — Skyflow FlowVault SDK (`pod 'Skyflow-flowvault-ios'` / `import SkyflowFlowVault`) +## [`SkyflowFlowVault/`](SkyflowFlowVault/) — Skyflow FlowVault SDK (`pod 'SkyflowFlowVault'` / `import SkyflowFlowVault`) | Sample | Demonstrates | |---|---| diff --git a/Skyflow-flowvault-ios.podspec b/SkyflowFlowVault.podspec similarity index 83% rename from Skyflow-flowvault-ios.podspec rename to SkyflowFlowVault.podspec index 3d93d96a..95681393 100644 --- a/Skyflow-flowvault-ios.podspec +++ b/SkyflowFlowVault.podspec @@ -1,16 +1,12 @@ Pod::Spec.new do |spec| - spec.name = "Skyflow-flowvault-ios" - - # Consumers import the SDK as `import SkyflowFlowVault` (Swift module names - # cannot contain hyphens). - spec.module_name = "SkyflowFlowVault" + spec.name = "SkyflowFlowVault" spec.version = "1.26.0-beta.1" - spec.summary = "skyflow-flowvault-iOS" + spec.summary = "SkyflowFlowVault" - spec.description = "Skyflow FlowVault iOS SDK (v2 API)" + spec.description = "Skyflow FlowVault iOS SDK" spec.homepage = "https://github.com/skyflowapi/skyflow-iOS.git" diff --git a/Sources/Skyflow/client/Client.swift b/Sources/Skyflow/client/Client.swift index a4e4665f..68d437b8 100644 --- a/Sources/Skyflow/client/Client.swift +++ b/Sources/Skyflow/client/Client.swift @@ -19,12 +19,11 @@ extension Client { } // MARK: - Legacy (v1) contract operations - public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .INSERT Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) @@ -92,7 +91,7 @@ extension Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .DETOKENIZE Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) @@ -135,7 +134,7 @@ extension Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .GETBYID Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_GET_BY_ID_INPUT, contextOptions: tempContextOptions) @@ -175,7 +174,7 @@ extension Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .GET Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) diff --git a/Sources/Skyflow/collect/CollectContainerOperations.swift b/Sources/Skyflow/collect/CollectContainerOperations.swift index 07c313c7..e6f1ef0a 100644 --- a/Sources/Skyflow/collect/CollectContainerOperations.swift +++ b/Sources/Skyflow/collect/CollectContainerOperations.swift @@ -11,7 +11,7 @@ public extension Container { func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -19,7 +19,7 @@ public extension Container { Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = RequestValidators.checkElement(element: element) + errorCode = CoreRequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return diff --git a/Sources/Skyflow/composable/ComposableContainerOperations.swift b/Sources/Skyflow/composable/ComposableContainerOperations.swift index 891d7c4b..eacfd283 100644 --- a/Sources/Skyflow/composable/ComposableContainerOperations.swift +++ b/Sources/Skyflow/composable/ComposableContainerOperations.swift @@ -12,7 +12,7 @@ public extension Container { func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -20,7 +20,7 @@ public extension Container { Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = RequestValidators.checkElement(element: element) + errorCode = CoreRequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return diff --git a/Sources/Skyflow/reveal/RevealContainerOperations.swift b/Sources/Skyflow/reveal/RevealContainerOperations.swift index c46a2f30..bfe583ff 100644 --- a/Sources/Skyflow/reveal/RevealContainerOperations.swift +++ b/Sources/Skyflow/reveal/RevealContainerOperations.swift @@ -10,7 +10,7 @@ public extension Container { func reveal(callback: Callback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errorCode: ErrorCodes? @@ -20,7 +20,7 @@ public extension Container { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return } - if let elementError = RequestValidators.checkRevealElements(elements: self.revealElements) { + if let elementError = CoreRequestValidators.checkRevealElements(elements: self.revealElements) { callback.onFailure(elementError.getErrorObject(contextOptions: tempContextOptions)) return } diff --git a/Sources/Skyflow/validations/RequestValidators.swift b/Sources/Skyflow/validations/RequestValidators.swift index 0c364577..70c5ce94 100644 --- a/Sources/Skyflow/validations/RequestValidators.swift +++ b/Sources/Skyflow/validations/RequestValidators.swift @@ -6,36 +6,12 @@ // container operations and the client-level operations. Stateless: only // static functions, never instantiated. Validators compute and return the // error; the call sites own delivering the failure to their callback. +// Contract-agnostic validators (checkClientConfig, checkElement, +// checkRevealElements) live in SkyflowCore.CoreRequestValidators. import Foundation internal class RequestValidators { - // Guards shared by every operation: the client must have been configured - // with a vault ID and URL. (An empty vaultURL becomes "/" in SkyflowCore.Client.) - internal static func checkClientConfig(vaultID: String, vaultURL: String) -> ErrorCodes? { - if vaultID.isEmpty { - return .EMPTY_VAULT_ID() - } - if vaultURL == "/" { - return .EMPTY_VAULT_URL() - } - return nil - } - - internal static func checkElement(element: TextField) -> ErrorCodes? { - if element.collectInput.tableName.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) - } - - return nil - } - internal static func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { if record["table"] == nil { return .TABLE_KEY_ERROR(value: "\(index)") @@ -60,18 +36,6 @@ internal class RequestValidators { return nil } - internal static func checkRevealElements(elements: [Label]) -> ErrorCodes? { - for element in elements { - if element.errorTriggered { - return .ERROR_TRIGGERED(value: element.triggeredErrorMessage) - } - if element.getToken().isEmpty { - return .EMPTY_TOKEN_ID() - } - } - return nil - } - internal static func checkDetokenizeRecord(token: [String: Any], index: Int) -> ErrorCodes? { if token["redaction"] != nil { guard let _ = token["redaction"] as? RedactionType else { diff --git a/Sources/SkyflowCore/composable/ComposableContainer.swift b/Sources/SkyflowCore/composable/ComposableContainer.swift index ee0d25cc..84937570 100644 --- a/Sources/SkyflowCore/composable/ComposableContainer.swift +++ b/Sources/SkyflowCore/composable/ComposableContainer.swift @@ -24,7 +24,7 @@ public extension Container { return skyflowElement } - func on(eventName: EventName, handler: @escaping () -> Void) { + func on(eventName: EventName, handler: @escaping () -> Void) -> Void where T: ComposableContainer { if (eventName == EventName.SUBMIT){ for element in elements { element.onSubmitHandler = handler @@ -179,7 +179,7 @@ public extension Container { } - func getComposableView() throws -> UIView { + func getComposableView() throws -> UIView where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER var totalCount = 0 diff --git a/Sources/SkyflowCore/validations/CoreRequestValidators.swift b/Sources/SkyflowCore/validations/CoreRequestValidators.swift new file mode 100644 index 00000000..a66b7225 --- /dev/null +++ b/Sources/SkyflowCore/validations/CoreRequestValidators.swift @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Pre-flight request validators shared by both SDKs, used by the client-level +// and container operations. Contract-specific validators stay in each SDK's +// own RequestValidators. Stateless: only static functions, never instantiated. +// Validators compute and return the error; the call sites own delivering the +// failure to their callback. + +import Foundation + +package class CoreRequestValidators { + // Guards shared by every operation: the client must have been configured + // with a vault ID and URL. (An empty vaultURL becomes "/" in SkyflowCore.Client.) + package static func checkClientConfig(vaultID: String, vaultURL: String) -> ErrorCodes? { + if vaultID.isEmpty { + return .EMPTY_VAULT_ID() + } + if vaultURL == "/" { + return .EMPTY_VAULT_URL() + } + return nil + } + + package static func checkElement(element: TextField) -> ErrorCodes? { + if element.collectInput.tableName.isEmpty { + return .EMPTY_TABLE_NAME_IN_COLLECT() + } + if element.collectInput.column.isEmpty { + return .EMPTY_COLUMN_NAME_IN_COLLECT() + } + if !element.isMounted() { + return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) + } + + return nil + } + + package static func checkRevealElements(elements: [Label]) -> ErrorCodes? { + for element in elements { + if element.errorTriggered { + return .ERROR_TRIGGERED(value: element.triggeredErrorMessage) + } + if element.getToken().isEmpty { + return .EMPTY_TOKEN_ID() + } + } + return nil + } +} diff --git a/Sources/SkyflowFlowVault/client/Client.swift b/Sources/SkyflowFlowVault/client/Client.swift index 29f15887..86af6890 100644 --- a/Sources/SkyflowFlowVault/client/Client.swift +++ b/Sources/SkyflowFlowVault/client/Client.swift @@ -23,7 +23,7 @@ public extension Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .INSERT Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errorCode: ErrorCodes? @@ -90,7 +90,7 @@ public extension Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .DETOKENIZE Log.info(message: .DETOKENIZE_TRIGGERED, contextOptions: tempContextOptions) - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.vaultID, vaultURL: self.vaultURL) { return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_DETOKENIZE_INPUT, contextOptions: tempContextOptions) diff --git a/Sources/SkyflowFlowVault/collect/CollectCallback.swift b/Sources/SkyflowFlowVault/collect/CollectCallback.swift new file mode 100644 index 00000000..9da3eeb4 --- /dev/null +++ b/Sources/SkyflowFlowVault/collect/CollectCallback.swift @@ -0,0 +1,33 @@ +// +// CollectCallback.swift +// Skyflow +// +// Created by Bharti Sagar on 13/08/26. +// + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into CollectResponse +// for you. Use it directly with the original callback-based collect(callback:options:). +public class CollectCallback: Callback { + private let successHandler: (CollectResponse) -> Void + private let failureHandler: (SkyflowError) -> Void + + public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = CollectResponse(responseBody) else { + failureHandler(SkyflowError.wrap(responseBody)) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-record failure inside CollectResponse.records, and for client-side validation + // failures. Always normalized into a Skyflow.SkyflowError - see SkyflowError.wrap. + public func onFailure(_ error: Any) { + failureHandler(SkyflowError.wrap(error)) + } +} diff --git a/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift b/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift index 055197f7..7e2f45b7 100644 --- a/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift +++ b/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift @@ -7,10 +7,13 @@ import Foundation import UIKit public extension Container { + func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: CollectContainer { + return makeCollectElement(input: input.data, options: options?.data) + } func collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -18,7 +21,7 @@ public extension Container { Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = RequestValidators.checkElement(element: element) + errorCode = CoreRequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return @@ -74,9 +77,3 @@ public extension Container { } } } - -public extension Container { - func create(input: CollectElementInput, options: CollectElementOptions? = CollectElementOptions()) -> TextField where T: CollectContainer { - return makeCollectElement(input: input.data, options: options?.data) - } -} diff --git a/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift b/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift index a253dbe5..6c359e76 100644 --- a/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift +++ b/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift @@ -11,7 +11,7 @@ public extension Container { func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errors = "" @@ -19,7 +19,7 @@ public extension Container { Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) for element in self.elements { - errorCode = RequestValidators.checkElement(element: element) + errorCode = CoreRequestValidators.checkElement(element: element) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return diff --git a/Sources/SkyflowFlowVault/models/CollectResponse.swift b/Sources/SkyflowFlowVault/models/CollectResponse.swift index 4aaea9f2..64656bd4 100644 --- a/Sources/SkyflowFlowVault/models/CollectResponse.swift +++ b/Sources/SkyflowFlowVault/models/CollectResponse.swift @@ -40,30 +40,3 @@ public struct CollectRecord { self.error = dict["error"] as? String } } - -// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into CollectResponse -// for you. Use it directly with the original callback-based collect(callback:options:). -public class CollectCallback: Callback { - private let successHandler: (CollectResponse) -> Void - private let failureHandler: (SkyflowError) -> Void - - public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { - self.successHandler = onSuccess - self.failureHandler = onFailure - } - - public func onSuccess(_ responseBody: Any) { - guard let response = CollectResponse(responseBody) else { - failureHandler(SkyflowError.wrap(responseBody)) - return - } - successHandler(response) - } - - // Delivered when the entire request fails (e.g. vault not found, network error) rather - // than a per-record failure inside CollectResponse.records, and for client-side validation - // failures. Always normalized into a Skyflow.SkyflowError - see SkyflowError.wrap. - public func onFailure(_ error: Any) { - failureHandler(SkyflowError.wrap(error)) - } -} diff --git a/Sources/SkyflowFlowVault/models/RevealResponse.swift b/Sources/SkyflowFlowVault/models/RevealResponse.swift index f9baa0c6..9730f432 100644 --- a/Sources/SkyflowFlowVault/models/RevealResponse.swift +++ b/Sources/SkyflowFlowVault/models/RevealResponse.swift @@ -56,33 +56,3 @@ public struct RevealRecord { return normalized } } - -// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into RevealResponse -// for you. Use it directly with the callback-based reveal(callback:options:) - RevealValueCallback -// merges success/failure into one "records" array (matching Collect's convention), so the same -// RevealResponse type used for Client.detokenize() applies here too. -public class RevealCallback: Callback { - private let successHandler: (RevealResponse) -> Void - private let failureHandler: (SkyflowError) -> Void - - public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { - self.successHandler = onSuccess - self.failureHandler = onFailure - } - - public func onSuccess(_ responseBody: Any) { - guard let response = RevealResponse(responseBody) else { - failureHandler(SkyflowError.wrap(responseBody)) - return - } - successHandler(response) - } - - // Delivered when the entire request fails (e.g. vault not found, network error) rather - // than a per-token failure inside RevealResponse.records, and for client-side validation - // failures (empty vaultID, unmounted element, etc.). Always normalized into a - // Skyflow.SkyflowError - see SkyflowError.wrap. - public func onFailure(_ error: Any) { - failureHandler(SkyflowError.wrap(error)) - } -} diff --git a/Sources/SkyflowFlowVault/reveal/RevealCallback.swift b/Sources/SkyflowFlowVault/reveal/RevealCallback.swift new file mode 100644 index 00000000..cc46d895 --- /dev/null +++ b/Sources/SkyflowFlowVault/reveal/RevealCallback.swift @@ -0,0 +1,36 @@ +// +// RevealCallback.swift +// Skyflow +// +// Created by Bharti Sagar on 13/08/26. +// + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into RevealResponse +// for you. Use it directly with the callback-based reveal(callback:options:) - RevealValueCallback +// merges success/failure into one "records" array (matching Collect's convention), so the same +// RevealResponse type used for Client.detokenize() applies here too. +public class RevealCallback: Callback { + private let successHandler: (RevealResponse) -> Void + private let failureHandler: (SkyflowError) -> Void + + public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = RevealResponse(responseBody) else { + failureHandler(SkyflowError.wrap(responseBody)) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-token failure inside RevealResponse.records, and for client-side validation + // failures (empty vaultID, unmounted element, etc.). Always normalized into a + // Skyflow.SkyflowError - see SkyflowError.wrap. + public func onFailure(_ error: Any) { + failureHandler(SkyflowError.wrap(error)) + } +} diff --git a/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift b/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift index 15dc805b..5e95f7cc 100644 --- a/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift +++ b/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift @@ -6,12 +6,14 @@ // token-group redactions). import Foundation - public extension Container { + func create(input: RevealElementInput, options: RevealElementOptions? = RevealElementOptions()) -> Label where T: RevealContainer { + return makeRevealElement(input: input.data, options: options?.data) + } func reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER - if let errorCode = RequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { + if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } var errorCode: ErrorCodes? @@ -21,7 +23,7 @@ public extension Container { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return } - if let elementError = RequestValidators.checkRevealElements(elements: self.revealElements) { + if let elementError = CoreRequestValidators.checkRevealElements(elements: self.revealElements) { callback.onFailure(elementError.getErrorObject(contextOptions: tempContextOptions)) return } @@ -52,9 +54,3 @@ public extension Container { } } } - -public extension Container { - func create(input: RevealElementInput, options: RevealElementOptions? = RevealElementOptions()) -> Label where T: RevealContainer { - return makeRevealElement(input: input.data, options: options?.data) - } -} diff --git a/Sources/SkyflowFlowVault/validations/RequestValidators.swift b/Sources/SkyflowFlowVault/validations/RequestValidators.swift index d7cbaf3d..61f49af2 100644 --- a/Sources/SkyflowFlowVault/validations/RequestValidators.swift +++ b/Sources/SkyflowFlowVault/validations/RequestValidators.swift @@ -5,36 +5,12 @@ // Pre-flight validators for the FlowDB v2 collect flows, shared by the // collect and composable container operations. Stateless: only static // functions, never instantiated. +// Contract-agnostic validators (checkClientConfig, checkElement, +// checkRevealElements) live in SkyflowCore.CoreRequestValidators. import Foundation internal class RequestValidators { - // Guards shared by every operation: the client must have been configured - // with a vault ID and URL. (An empty vaultURL becomes "/" in SkyflowCore.Client.) - internal static func checkClientConfig(vaultID: String, vaultURL: String) -> ErrorCodes? { - if vaultID.isEmpty { - return .EMPTY_VAULT_ID() - } - if vaultURL == "/" { - return .EMPTY_VAULT_URL() - } - return nil - } - - internal static func checkElement(element: TextField) -> ErrorCodes? { - if element.collectInput.tableName.isEmpty { - return .EMPTY_TABLE_NAME_IN_COLLECT() - } - if element.collectInput.column.isEmpty { - return .EMPTY_COLUMN_NAME_IN_COLLECT() - } - if !element.isMounted() { - return .UNMOUNTED_COLLECT_ELEMENT(value: element.collectInput.column) - } - - return nil - } - internal static func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { if record.tableName.isEmpty { return .EMPTY_TABLE_NAME() @@ -45,18 +21,6 @@ internal class RequestValidators { return nil } - internal static func checkRevealElements(elements: [Label]) -> ErrorCodes? { - for element in elements { - if element.errorTriggered { - return .ERROR_TRIGGERED(value: element.triggeredErrorMessage) - } - if element.getToken().isEmpty { - return .EMPTY_TOKEN_ID() - } - } - return nil - } - internal static func checkTokenGroupRedactions(_ redactions: [TokenGroupRedaction]) -> ErrorCodes? { for (index, entry) in redactions.enumerated() { if entry.tokenGroupName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 7fb57631..e6542de6 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -1,7 +1,7 @@ # Both SDK products share one version line: this bumps Skyflow.podspec, -# Skyflow-flowvault-ios.podspec, and the shared SDK_VERSION together. +# SkyflowFlowVault.podspec, and the shared SDK_VERSION together. # # Usage: # ./scripts/bump_version.sh 1.27.0 # release version, tag 1.27.0 @@ -10,7 +10,7 @@ version=$1 SEMVER=$version -PODSPECS="Skyflow.podspec Skyflow-flowvault-ios.podspec" +PODSPECS="Skyflow.podspec SkyflowFlowVault.podspec" VERSION_FILE="./Sources/SkyflowCore/Version.swift" if [ -z $2 ] From 70135dc275e1a53c782ebbd5c66f0149e2d0f413 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 14 Aug 2026 10:00:19 +0530 Subject: [PATCH 05/16] SK-3043 move common validators --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index f9497ab4..1f76cc31 100644 --- a/Package.swift +++ b/Package.swift @@ -1,3 +1,4 @@ +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From e463ac0f8be28325ba5e3a1ed2ec762ab9035c9c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 14 Aug 2026 17:51:18 +0530 Subject: [PATCH 06/16] SK-3043 add type for tokens --- Package.swift | 37 +- Skyflow.podspec | 2 +- SkyflowFlowVault.podspec | 2 +- Sources/Skyflow/client/Client.swift | 27 +- .../collect/CollectContainerOperations.swift | 53 +-- .../ComposableContainerOperations.swift | 54 +-- .../reveal/RevealContainerOperations.swift | 8 +- .../validations/RequestValidators.swift | 21 ++ .../validations/CoreRequestValidators.swift | 74 ++++ Sources/SkyflowFlowVault/client/Client.swift | 27 +- .../collect/CollectContainerOperations.swift | 41 +- .../ComposableContainerOperations.swift | 42 +-- .../models/CollectResponse.swift | 12 +- Sources/SkyflowFlowVault/models/Token.swift | 27 ++ .../reveal/RevealContainerOperations.swift | 8 +- .../validations/RequestValidators.swift | 14 + .../skyflow-iOS-getByIdTests/DemoImpl.swift | 0 .../XCTestManifests.swift | 4 +- .../skyflow_iOS_getByIdTests.swift | 0 .../skyflow_iOS_getByIdUtilTests.swift | 7 +- .../skyflow-iOS-getTests/DemoImpl.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_getTests.swift | 0 .../skyflow_iOS_getUtilTests.swift | 7 +- .../skyflow_iOS_validateGetRecordTest.swift | 0 .../CoreRequestValidatorsTests.swift | 222 +++++++++++ .../skyflow-iOS-legacyTests/DemoImpl.swift | 0 .../MockURLProtocol.swift | 0 ...w_iOS_dormantCollectApiCallbackTests.swift | 0 ...w_iOS_dormantCollectRequestBodyTests.swift | 0 .../skyflow_iOS_legacyClientApiTests.swift | 354 ++++++++++++++++++ ...egacyCollectApiCallbackCoverageTests.swift | 210 +++++++++++ .../skyflow_iOS_legacyCollectOpsTests.swift | 274 ++++++++++++++ .../skyflow_iOS_legacyRevealOpsTests.swift | 227 +++++++++++ .../skyflow_iOS_legacyUtilTests.swift | 0 .../skyflow_iOS_legacyValidatorTests.swift | 202 ++++++++++ .../skyflow_iOS_revealApiCallbackTests.swift | 0 .../ClientScenario.swift | 0 .../skyflow-iOS-scenarioTests/DemoImpl.swift | 0 .../DetokenizeScenario.swift | 0 .../GetByIdScenario.swift | 0 .../InsertScenario.swift | 0 .../skyflow-iOS-scenarioTests/TestData.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_detokenizeScenarioTests.swift | 0 .../skyflow_iOS_getByIdScenarioTests.swift | 0 .../skyflow_iOS_insertScenarioTests.swift | 0 .../skyflow-iOS-collectTests/DemoImpl.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_collectTests.swift | 0 ...ow_iOS_emptySkyflowIdValidationTests.swift | 0 .../DemoImpl.swift | 0 ...skyflow_iOS_composableEelementsTests.swift | 20 + .../skyflow_iOS_composableFlowTests.swift | 86 +++++ .../CollectElementOptionsTests.swift | 0 .../skyflow-iOS-elementTests/DemoImpl.swift | 0 .../InputFormattingTests.swift | 0 .../TextFieldDelegateTests.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_containerTests.swift | 0 .../skyflow_iOS_elementTests.swift | 0 .../skyflow-iOS-errorTests/DemoImpl.swift | 0 .../skyflow-iOS-errorTests/GatewayImpl.swift | 0 .../Skyflow_iOS_collectErrorTests.swift | 0 .../Skyflow_iOS_generalErrorTests.swift | 0 .../Skyflow_iOS_revealErrorTests.swift | 0 ...low_iOS_revealOptionsValidationTests.swift | 0 .../XCTestManifests.swift | 0 .../skyflow-iOS-revealTests/DemoImpl.swift | 0 .../RevealElementOptionsTests.swift | 0 .../RevealSkyflowLabelViewTests.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_revealTests.swift | 0 .../skyflow-iOS-utilTests/DemoImpl.swift | 0 .../MockURLProtocol.swift | 0 .../TokenLifecycleTests.swift | 158 ++++++++ .../TokenParsingTests.swift | 164 ++++++++ .../skyflow_iOS_collectUtilTests.swift | 0 .../skyflow_iOS_cvvMockTests.swift | 0 .../skyflow_iOS_flowVaultSdkTests.swift | 189 ++++++++++ .../skyflow_iOS_insertUtilTests.swift} | 0 .../skyflow_iOS_revealUtilTests.swift | 0 .../skyflow_iOS_utilTests.swift | 0 83 files changed, 2326 insertions(+), 247 deletions(-) create mode 100644 Sources/SkyflowFlowVault/models/Token.swift rename Tests/{ => Skyflow}/skyflow-iOS-getByIdTests/DemoImpl.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-getByIdTests/XCTestManifests.swift (65%) rename Tests/{ => Skyflow}/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift (97%) rename Tests/{ => Skyflow}/skyflow-iOS-getTests/DemoImpl.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-getTests/XCTestManifests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-getTests/skyflow_iOS_getTests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift (98%) rename Tests/{ => Skyflow}/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift (100%) create mode 100644 Tests/Skyflow/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift rename Tests/{ => Skyflow}/skyflow-iOS-legacyTests/DemoImpl.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-legacyTests/MockURLProtocol.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift (100%) create mode 100644 Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift create mode 100644 Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift create mode 100644 Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift create mode 100644 Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift rename Tests/{ => Skyflow}/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift (100%) create mode 100644 Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift rename Tests/{ => Skyflow}/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/ClientScenario.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/DemoImpl.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/DetokenizeScenario.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/GetByIdScenario.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/InsertScenario.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/TestData.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/XCTestManifests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift (100%) rename Tests/{ => Skyflow}/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-collectTests/DemoImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-collectTests/XCTestManifests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-composableTests/DemoImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift (96%) create mode 100644 Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/CollectElementOptionsTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/DemoImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/InputFormattingTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/TextFieldDelegateTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/XCTestManifests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/DemoImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/GatewayImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-errorTests/XCTestManifests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-revealTests/DemoImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-revealTests/RevealElementOptionsTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-revealTests/XCTestManifests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-utilTests/DemoImpl.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-utilTests/MockURLProtocol.swift (100%) create mode 100644 Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenLifecycleTests.swift create mode 100644 Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenParsingTests.swift rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift (100%) create mode 100644 Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift rename Tests/{skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift => SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift} (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift (100%) rename Tests/{ => SkyflowFlowVault}/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift (100%) diff --git a/Package.swift b/Package.swift index 1f76cc31..237fe023 100644 --- a/Package.swift +++ b/Package.swift @@ -9,11 +9,11 @@ let package = Package( .iOS(.v13) ], products: [ - // PDB vault SDK 1(v1 API contract). + // PDB vault SDK .library( name: "Skyflow", targets: ["Skyflow"]), - // FlowVault SDK (v2 API contract). + // FlowVault SDK .library( name: "SkyflowFlowVault", targets: ["SkyflowFlowVault"]), @@ -40,25 +40,38 @@ let package = Package( name: "SkyflowFlowVault", dependencies: ["SkyflowCore"] ), + // Tests for the legacy (v1) SDK live under Tests/Skyflow/, + // FlowVault (v2) SDK tests under Tests/SkyflowFlowVault/. .testTarget( name: "skyflow-iOS-collectTests", - dependencies: ["SkyflowFlowVault", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"], + path: "Tests/SkyflowFlowVault/skyflow-iOS-collectTests"), .testTarget(name: "skyflow-iOS-revealTests", - dependencies: ["SkyflowFlowVault", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"], + path: "Tests/SkyflowFlowVault/skyflow-iOS-revealTests"), .testTarget(name: "skyflow-iOS-errorTests", - dependencies: ["SkyflowFlowVault", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"], + path: "Tests/SkyflowFlowVault/skyflow-iOS-errorTests"), .testTarget(name: "skyflow-iOS-getByIdTests", - dependencies: ["Skyflow", "SkyflowCore"]), + dependencies: ["Skyflow", "SkyflowCore"], + path: "Tests/Skyflow/skyflow-iOS-getByIdTests"), .testTarget(name: "skyflow-iOS-elementTests", - dependencies: ["SkyflowFlowVault", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"], + path: "Tests/SkyflowFlowVault/skyflow-iOS-elementTests"), .testTarget(name: "skyflow-iOS-utilTests", - dependencies: ["SkyflowFlowVault", "SkyflowCore"]), + dependencies: ["SkyflowFlowVault", "SkyflowCore"], + path: "Tests/SkyflowFlowVault/skyflow-iOS-utilTests"), .testTarget(name: "skyflow-iOS-legacyTests", - dependencies: ["Skyflow", "SkyflowCore"]), + dependencies: ["Skyflow", "SkyflowCore"], + path: "Tests/Skyflow/skyflow-iOS-legacyTests"), .testTarget(name: "skyflow-iOS-scenarioTests", - dependencies: ["Skyflow", "SkyflowCore"]), + dependencies: ["Skyflow", "SkyflowCore"], + path: "Tests/Skyflow/skyflow-iOS-scenarioTests"), .testTarget(name: "skyflow-iOS-getTests", - dependencies: ["Skyflow", "SkyflowCore"]), - .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"]) + dependencies: ["Skyflow", "SkyflowCore"], + path: "Tests/Skyflow/skyflow-iOS-getTests"), + .testTarget(name: "skyflow-iOS-composableTests", + dependencies: ["SkyflowFlowVault", "SkyflowCore"], + path: "Tests/SkyflowFlowVault/skyflow-iOS-composableTests") ] ) diff --git a/Skyflow.podspec b/Skyflow.podspec index a2d2223e..9c87c678 100644 --- a/Skyflow.podspec +++ b/Skyflow.podspec @@ -6,7 +6,7 @@ Pod::Spec.new do |spec| spec.summary = "skyflow-iOS" - spec.description = "Skyflow iOS SDK (legacy v1 vault API)" + spec.description = "Skyflow iOS SDK" spec.homepage = "https://github.com/skyflowapi/skyflow-iOS.git" diff --git a/SkyflowFlowVault.podspec b/SkyflowFlowVault.podspec index 95681393..4bafe7d0 100644 --- a/SkyflowFlowVault.podspec +++ b/SkyflowFlowVault.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |spec| spec.license = { :type => "MIT", :file => "LICENSE" } - spec.author = { "Skyflow" => "service-ops@skyflow.com" } + spec.author = { "Skyflow" => "service-ops@skyflow.com" } spec.swift_version = '5.9' diff --git a/Sources/Skyflow/client/Client.swift b/Sources/Skyflow/client/Client.swift index 68d437b8..42e9bd31 100644 --- a/Sources/Skyflow/client/Client.swift +++ b/Sources/Skyflow/client/Client.swift @@ -37,32 +37,7 @@ extension Client { Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) if let recordEntries = records["records"] as? [[String: Any]] { - for (index, record) in recordEntries.enumerated() { - if record["table"] != nil { - if !(record["table"] is String) { - errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } else { - if (record["table"] as! String).isEmpty { - errorCode = .EMPTY_TABLE_NAME() - } else { - if record["fields"] != nil { - if !(record["fields"] is [String: Any]) { - errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") - break - } - let fields = record["fields"] as! [String: Any] - if fields.isEmpty { - errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") - } - } else { - errorCode = .FIELDS_KEY_ERROR(value: "\(index)") - } - } - } - } else { - errorCode = .TABLE_KEY_ERROR(value: "\(index)") - } - } + errorCode = CoreRequestValidators.checkInsertRecordEntries(recordEntries) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return diff --git a/Sources/Skyflow/collect/CollectContainerOperations.swift b/Sources/Skyflow/collect/CollectContainerOperations.swift index e6f1ef0a..8c6183fd 100644 --- a/Sources/Skyflow/collect/CollectContainerOperations.swift +++ b/Sources/Skyflow/collect/CollectContainerOperations.swift @@ -14,55 +14,20 @@ public extension Container { if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - var errors = "" - var errorCode: ErrorCodes? Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) - for element in self.elements { - errorCode = CoreRequestValidators.checkElement(element: element) - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - - let state = element.getState() - let error = state["validationError"] - if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { - errors += element.columnName + " is empty" + "\n" - element.updateErrorMessage() - } - if !(state["isValid"] as! Bool) { - errors += "for " + element.columnName + " " + (error as! String) + "\n" - } - if element.isFirstResponder { - element.resignFirstResponder() - } + let elementsValidation = CoreRequestValidators.validateElementStates(elements: self.elements) + if let elementErrorCode = elementsValidation.errorCode { + callback.onFailure(elementErrorCode.getErrorObject(contextOptions: tempContextOptions)) + return } - if errors != "" { - callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + if elementsValidation.errors != "" { + callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: elementsValidation.errors])) return } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = RequestValidators.checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return + if let additionalFields = options?.additionalFields { + if let errorCode = RequestValidators.checkAdditionalFields(additionalFields) { + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } } let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) diff --git a/Sources/Skyflow/composable/ComposableContainerOperations.swift b/Sources/Skyflow/composable/ComposableContainerOperations.swift index eacfd283..4c8076f0 100644 --- a/Sources/Skyflow/composable/ComposableContainerOperations.swift +++ b/Sources/Skyflow/composable/ComposableContainerOperations.swift @@ -15,57 +15,21 @@ public extension Container { if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - var errors = "" - var errorCode: ErrorCodes? Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) - for element in self.elements { - errorCode = CoreRequestValidators.checkElement(element: element) - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - - let state = element.getState() - let error = state["validationError"] - if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { - errors += element.columnName + " is empty" + "\n" - element.updateErrorMessage() - } - if !(state["isValid"] as! Bool) { - errors += "for " + element.columnName + " " + (error as! String) + "\n" - } - if element.isFirstResponder { - element.resignFirstResponder() - } + let elementsValidation = CoreRequestValidators.validateElementStates(elements: self.elements) + if let elementErrorCode = elementsValidation.errorCode { + callback.onFailure(elementErrorCode.getErrorObject(contextOptions: tempContextOptions)) + return } - - if errors != "" { - callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + if elementsValidation.errors != "" { + callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: elementsValidation.errors])) return } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = RequestValidators.checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return + if let additionalFields = options?.additionalFields { + if let errorCode = RequestValidators.checkAdditionalFields(additionalFields) { + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } } let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) diff --git a/Sources/Skyflow/reveal/RevealContainerOperations.swift b/Sources/Skyflow/reveal/RevealContainerOperations.swift index bfe583ff..ada8ab9e 100644 --- a/Sources/Skyflow/reveal/RevealContainerOperations.swift +++ b/Sources/Skyflow/reveal/RevealContainerOperations.swift @@ -13,14 +13,8 @@ public extension Container { if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - var errorCode: ErrorCodes? Log.info(message: .VALIDATE_REVEAL_RECORDS, contextOptions: tempContextOptions) - if let element = ConversionHelpers.checkElementsAreMounted(elements: self.revealElements) as? Label { - errorCode = .UNMOUNTED_REVEAL_ELEMENT(value: element.revealInput.token) - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - if let elementError = CoreRequestValidators.checkRevealElements(elements: self.revealElements) { + if let elementError = CoreRequestValidators.checkRevealElementsPreflight(elements: self.revealElements) { callback.onFailure(elementError.getErrorObject(contextOptions: tempContextOptions)) return } diff --git a/Sources/Skyflow/validations/RequestValidators.swift b/Sources/Skyflow/validations/RequestValidators.swift index 70c5ce94..c2412893 100644 --- a/Sources/Skyflow/validations/RequestValidators.swift +++ b/Sources/Skyflow/validations/RequestValidators.swift @@ -36,6 +36,27 @@ internal class RequestValidators { return nil } + // Validation of the untyped additional fields dictionary passed to + // collect(), shared by the collect and composable container operations. + internal static func checkAdditionalFields(_ additionalFields: [String: Any]) -> ErrorCodes? { + if additionalFields["records"] == nil { + return .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() + } + if let additionalFieldEntries = additionalFields["records"] as? [[String: Any]] { + if additionalFieldEntries.isEmpty { + return .EMPTY_RECORDS_OBJECT() + } + for (index, record) in additionalFieldEntries.enumerated() { + if let errorCode = checkRecord(record: record, index: index) { + return errorCode + } + } + } else { + return .INVALID_RECORDS_TYPE() + } + return nil + } + internal static func checkDetokenizeRecord(token: [String: Any], index: Int) -> ErrorCodes? { if token["redaction"] != nil { guard let _ = token["redaction"] as? RedactionType else { diff --git a/Sources/SkyflowCore/validations/CoreRequestValidators.swift b/Sources/SkyflowCore/validations/CoreRequestValidators.swift index a66b7225..fe4a89ab 100644 --- a/Sources/SkyflowCore/validations/CoreRequestValidators.swift +++ b/Sources/SkyflowCore/validations/CoreRequestValidators.swift @@ -48,4 +48,78 @@ package class CoreRequestValidators { } return nil } + + // Element-state validation shared by the collect() and composable collect() + // operations. Returns the fail-fast element error (if any) plus the + // accumulated per-element validation messages; the call sites own wrapping + // the messages in their error type and delivering to their callback. + // Side effects (updateErrorMessage, resignFirstResponder) happen while + // iterating, exactly as in the original per-SDK loops. + package static func validateElementStates(elements: [TextField]) -> (errorCode: ErrorCodes?, errors: String) { + var errors = "" + for element in elements { + let errorCode = checkElement(element: element) + if errorCode != nil { + return (errorCode, errors) + } + + + let state = element.getState() + let error = state["validationError"] + if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { + errors += element.columnName + " is empty" + "\n" + element.updateErrorMessage() + } + if !(state["isValid"] as! Bool) { + errors += "for " + element.columnName + " " + (error as! String) + "\n" + } + if element.isFirstResponder { + element.resignFirstResponder() + } + } + return (nil, errors) + } + + // Reveal pre-flight shared by both SDKs' reveal() operations: all elements + // must be mounted, error-free, and carry a token. + package static func checkRevealElementsPreflight(elements: [Label]) -> ErrorCodes? { + if let element = ConversionHelpers.checkElementsAreMounted(elements: elements) as? Label { + return .UNMOUNTED_REVEAL_ELEMENT(value: element.revealInput.token) + } + return checkRevealElements(elements: elements) + } + + // Insert record-entry validation shared by both SDKs' client insert() + // operations. Preserves the original loop's semantics exactly: the last + // entry's error wins, except an invalid fields type stops the scan. + package static func checkInsertRecordEntries(_ recordEntries: [[String: Any]]) -> ErrorCodes? { + var errorCode: ErrorCodes? + for (index, record) in recordEntries.enumerated() { + if record["table"] != nil { + if !(record["table"] is String) { + errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") + } else { + if (record["table"] as! String).isEmpty { + errorCode = .EMPTY_TABLE_NAME() + } else { + if record["fields"] != nil { + if !(record["fields"] is [String: Any]) { + errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") + break + } + let fields = record["fields"] as! [String: Any] + if fields.isEmpty { + errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") + } + } else { + errorCode = .FIELDS_KEY_ERROR(value: "\(index)") + } + } + } + } else { + errorCode = .TABLE_KEY_ERROR(value: "\(index)") + } + } + return errorCode + } } diff --git a/Sources/SkyflowFlowVault/client/Client.swift b/Sources/SkyflowFlowVault/client/Client.swift index 86af6890..876cd6cb 100644 --- a/Sources/SkyflowFlowVault/client/Client.swift +++ b/Sources/SkyflowFlowVault/client/Client.swift @@ -36,32 +36,7 @@ public extension Client { Log.info(message: .VALIDATE_RECORDS, contextOptions: tempContextOptions) if let recordEntries = records["records"] as? [[String: Any]] { - for (index, record) in recordEntries.enumerated() { - if record["table"] != nil { - if !(record["table"] is String) { - errorCode = .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } else { - if (record["table"] as! String).isEmpty { - errorCode = .EMPTY_TABLE_NAME() - } else { - if record["fields"] != nil { - if !(record["fields"] is [String: Any]) { - errorCode = .INVALID_FIELDS_TYPE(value: "\(index)") - break - } - let fields = record["fields"] as! [String: Any] - if fields.isEmpty { - errorCode = .EMPTY_FIELDS_KEY(value: "\(index)") - } - } else { - errorCode = .FIELDS_KEY_ERROR(value: "\(index)") - } - } - } - } else { - errorCode = .TABLE_KEY_ERROR(value: "\(index)") - } - } + errorCode = CoreRequestValidators.checkInsertRecordEntries(recordEntries) if errorCode != nil { callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) return diff --git a/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift b/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift index 7e2f45b7..e1d21ebb 100644 --- a/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift +++ b/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift @@ -16,45 +16,20 @@ public extension Container { if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - var errors = "" - var errorCode: ErrorCodes? Log.info(message: .VALIDATE_COLLECT_RECORDS, contextOptions: tempContextOptions) - for element in self.elements { - errorCode = CoreRequestValidators.checkElement(element: element) - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - - let state = element.getState() - let error = state["validationError"] - if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { - errors += element.columnName + " is empty" + "\n" - element.updateErrorMessage() - } - if !(state["isValid"] as! Bool) { - errors += "for " + element.columnName + " " + (error as! String) + "\n" - } - if element.isFirstResponder { - element.resignFirstResponder() - } + let elementsValidation = CoreRequestValidators.validateElementStates(elements: self.elements) + if let elementErrorCode = elementsValidation.errorCode { + callback.onFailure(elementErrorCode.getErrorObject(contextOptions: tempContextOptions)) + return } - if errors != "" { - callback.onFailure(SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + if elementsValidation.errors != "" { + callback.onFailure(SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: elementsValidation.errors])) return } if let additionalFields = options?.additionalFields { - if additionalFields.records.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - for (index, record) in additionalFields.records.enumerated() { - errorCode = RequestValidators.checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } + if let errorCode = RequestValidators.checkAdditionalFields(additionalFields) { + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } } let records = CollectRequestBuilder.createCollectRecords(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) diff --git a/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift b/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift index 6c359e76..c65c8c98 100644 --- a/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift +++ b/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift @@ -14,47 +14,21 @@ public extension Container { if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - var errors = "" - var errorCode: ErrorCodes? Log.info(message: .VALIDATE_COMPOSABLE_RECORDS, contextOptions: tempContextOptions) - for element in self.elements { - errorCode = CoreRequestValidators.checkElement(element: element) - if errorCode != nil { - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - - - let state = element.getState() - let error = state["validationError"] - if (state["isRequired"] as! Bool) && (state["isEmpty"] as! Bool) { - errors += element.columnName + " is empty" + "\n" - element.updateErrorMessage() - } - if !(state["isValid"] as! Bool) { - errors += "for " + element.columnName + " " + (error as! String) + "\n" - } - if element.isFirstResponder { - element.resignFirstResponder() - } + let elementsValidation = CoreRequestValidators.validateElementStates(elements: self.elements) + if let elementErrorCode = elementsValidation.errorCode { + callback.onFailure(elementErrorCode.getErrorObject(contextOptions: tempContextOptions)) + return } - - if errors != "" { - callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + if elementsValidation.errors != "" { + callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: elementsValidation.errors])) return } if let additionalFields = options?.additionalFields { - if additionalFields.records.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - for (index, record) in additionalFields.records.enumerated() { - errorCode = RequestValidators.checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } + if let errorCode = RequestValidators.checkAdditionalFields(additionalFields) { + return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } } let records = CollectRequestBuilder.createCollectRecords(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) diff --git a/Sources/SkyflowFlowVault/models/CollectResponse.swift b/Sources/SkyflowFlowVault/models/CollectResponse.swift index 64656bd4..312be434 100644 --- a/Sources/SkyflowFlowVault/models/CollectResponse.swift +++ b/Sources/SkyflowFlowVault/models/CollectResponse.swift @@ -20,8 +20,9 @@ public struct CollectResponse { public struct CollectRecord { public let tableName: String? public let skyflowId: String? - // Keyed by column name, e.g. "card_number": [{"token": "...", "tokenGroupName": "..."}]. - public let tokens: [String: Any]? + // Keyed by column name, e.g. "card_number": [Token(token: "...", tokenGroupName: "...")]. + // A column maps to more than one Token when multiple token groups apply to it. + public let tokens: [String: [Token]]? // Keyed by column name, e.g. "card_number": [{"data": "...", "hashName": "..."}]. public let hashedData: [String: Any]? public let httpCode: Int @@ -33,8 +34,11 @@ public struct CollectRecord { // property name changed, not the response dict this reads from. self.skyflowId = dict["skyflowID"] as? String // Dict key is intentionally left as "fields" here - only the public Swift-facing - // property name changed, not the response dict this reads from. - self.tokens = dict["fields"] as? [String: Any] + // property name changed, not the response dict this reads from. Malformed entries + // (wrong shape, missing "token") are dropped rather than crashing. + self.tokens = (dict["fields"] as? [String: Any])?.compactMapValues { value in + (value as? [[String: Any]])?.compactMap(Token.init(dict:)) + } self.hashedData = dict["hashedData"] as? [String: Any] self.httpCode = dict["httpCode"] as? Int ?? 0 self.error = dict["error"] as? String diff --git a/Sources/SkyflowFlowVault/models/Token.swift b/Sources/SkyflowFlowVault/models/Token.swift new file mode 100644 index 00000000..4cf66ba9 --- /dev/null +++ b/Sources/SkyflowFlowVault/models/Token.swift @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// A single tokenized value for a collected field. A column can map to more than +// one Token when multiple token groups apply to the same field (e.g. deterministic +// and vault tokens both configured for the same column). + +import Foundation + +public struct Token { + public let token: String + public let tokenGroupName: String? + + public init(token: String, tokenGroupName: String? = nil) { + self.token = token + self.tokenGroupName = tokenGroupName + } + + // Parses one wire entry from a column's token array, e.g. {"token": "...", "tokenGroupName": "..."}. + // Drops the entry (returns nil) rather than crashing if "token" is missing or isn't a String. + init?(dict: [String: Any]) { + guard let token = dict["token"] as? String else { return nil } + self.token = token + self.tokenGroupName = dict["tokenGroupName"] as? String + } +} diff --git a/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift b/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift index 5e95f7cc..44edcf06 100644 --- a/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift +++ b/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift @@ -16,14 +16,8 @@ public extension Container { if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - var errorCode: ErrorCodes? Log.info(message: .VALIDATE_REVEAL_RECORDS, contextOptions: tempContextOptions) - if let element = ConversionHelpers.checkElementsAreMounted(elements: self.revealElements) as? Label { - errorCode = .UNMOUNTED_REVEAL_ELEMENT(value: element.revealInput.token) - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return - } - if let elementError = CoreRequestValidators.checkRevealElements(elements: self.revealElements) { + if let elementError = CoreRequestValidators.checkRevealElementsPreflight(elements: self.revealElements) { callback.onFailure(elementError.getErrorObject(contextOptions: tempContextOptions)) return } diff --git a/Sources/SkyflowFlowVault/validations/RequestValidators.swift b/Sources/SkyflowFlowVault/validations/RequestValidators.swift index 61f49af2..a22720b3 100644 --- a/Sources/SkyflowFlowVault/validations/RequestValidators.swift +++ b/Sources/SkyflowFlowVault/validations/RequestValidators.swift @@ -21,6 +21,20 @@ internal class RequestValidators { return nil } + // Validation of the typed additional fields passed to collect(), shared by + // the collect and composable container operations. + internal static func checkAdditionalFields(_ additionalFields: AdditionalFields) -> ErrorCodes? { + if additionalFields.records.isEmpty { + return .EMPTY_RECORDS_OBJECT() + } + for (index, record) in additionalFields.records.enumerated() { + if let errorCode = checkRecord(record: record, index: index) { + return errorCode + } + } + return nil + } + internal static func checkTokenGroupRedactions(_ redactions: [TokenGroupRedaction]) -> ErrorCodes? { for (index, entry) in redactions.enumerated() { if entry.tokenGroupName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty diff --git a/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-getByIdTests/DemoImpl.swift rename to Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-getByIdTests/XCTestManifests.swift b/Tests/Skyflow/skyflow-iOS-getByIdTests/XCTestManifests.swift similarity index 65% rename from Tests/skyflow-iOS-getByIdTests/XCTestManifests.swift rename to Tests/Skyflow/skyflow-iOS-getByIdTests/XCTestManifests.swift index 41f44b8c..add6193b 100644 --- a/Tests/skyflow-iOS-getByIdTests/XCTestManifests.swift +++ b/Tests/Skyflow/skyflow-iOS-getByIdTests/XCTestManifests.swift @@ -6,8 +6,6 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(skyflow_iOS_revealTests.allTests) - ] + return [] } #endif diff --git a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift b/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift similarity index 100% rename from Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift rename to Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift diff --git a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift b/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift similarity index 97% rename from Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift rename to Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift index 26933df9..66ead827 100644 --- a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift +++ b/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift @@ -227,9 +227,12 @@ final class skyflow_iOS_getByIdUtilTests: XCTestCase { let output = [["skyflow_id": "SID", "value": "val"]] self.revealApiCallback.handleCallbacks(outputArray: output, errorArray: [], isSuccess: true, errorObject: nil) - + wait(for: [expectation], timeout: 20.0) - let records = callback.data["records"] as! [[String: String]] + // This target's DemoAPICallback serializes success payloads into + // receivedResponse as a JSON string; data is only populated on failure. + let json = try! JSONSerialization.jsonObject(with: Data(callback.receivedResponse.utf8)) as! [String: Any] + let records = json["records"] as! [[String: String]] XCTAssertEqual(records.count, 1) XCTAssertEqual(records[0], output[0]) } diff --git a/Tests/skyflow-iOS-getTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-getTests/DemoImpl.swift rename to Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-getTests/XCTestManifests.swift b/Tests/Skyflow/skyflow-iOS-getTests/XCTestManifests.swift similarity index 100% rename from Tests/skyflow-iOS-getTests/XCTestManifests.swift rename to Tests/Skyflow/skyflow-iOS-getTests/XCTestManifests.swift diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift similarity index 100% rename from Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift rename to Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift similarity index 98% rename from Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift rename to Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift index 6c73edc3..9ee1c9d2 100644 --- a/Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift +++ b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift @@ -374,9 +374,12 @@ final class skyflow_iOS_getUtilTests: XCTestCase { let output = [["skyflow_id": "SID", "value": "val"]] self.getApiCallback.handleCallbacks(outputArray: output, errorArray: [], isSuccess: true, errorObject: nil) - + wait(for: [expectation], timeout: 20.0) - let records = callback.data["records"] as! [[String: String]] + // This target's DemoAPICallback serializes success payloads into + // receivedResponse as a JSON string; data is only populated on failure. + let json = try! JSONSerialization.jsonObject(with: Data(callback.receivedResponse.utf8)) as! [String: Any] + let records = json["records"] as! [[String: String]] XCTAssertEqual(records.count, 1) XCTAssertEqual(records[0], output[0]) } diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift similarity index 100% rename from Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift rename to Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift new file mode 100644 index 00000000..94c75fa7 --- /dev/null +++ b/Tests/Skyflow/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Direct tests for SkyflowCore.CoreRequestValidators: the contract-agnostic +// pre-flight validators shared by both SDKs' client and container operations +// (introduced by the SK-3043 common-code refactor). Exercised here via the +// legacy (v1) Client/Container since the functions themselves are contract-agnostic. + +import UIKit +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class CoreRequestValidatorsTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration( + vaultID: "vault_id", + vaultURL: "https://example.org/", + tokenProvider: DemoTokenProvider(), + options: Options(logLevel: .DEBUG))) + } + + override func tearDown() { + skyflow = nil + } + + private func assertError(_ actual: ErrorCodes?, _ expected: ErrorCodes) { + XCTAssertEqual(actual?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + expected.getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + // MARK: - checkClientConfig + + func testCheckClientConfigValid() { + XCTAssertNil(CoreRequestValidators.checkClientConfig(vaultID: "vault_id", vaultURL: "https://example.org/")) + } + + func testCheckClientConfigEmptyVaultID() { + assertError(CoreRequestValidators.checkClientConfig(vaultID: "", vaultURL: "https://example.org/"), + .EMPTY_VAULT_ID()) + } + + func testCheckClientConfigEmptyVaultURL() { + // An empty vaultURL is normalized to "/" by SkyflowCore.Client before reaching validators. + assertError(CoreRequestValidators.checkClientConfig(vaultID: "vault_id", vaultURL: "/"), + .EMPTY_VAULT_URL()) + } + + // MARK: - checkElement + + func testCheckElementValidMountedElement() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + UIWindow().addSubview(element) + + XCTAssertNil(CoreRequestValidators.checkElement(element: element)) + } + + func testCheckElementEmptyTableName() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "", column: "name", type: .CARDHOLDER_NAME)) + + assertError(CoreRequestValidators.checkElement(element: element), .EMPTY_TABLE_NAME_IN_COLLECT()) + } + + func testCheckElementEmptyColumnName() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "", type: .CARDHOLDER_NAME)) + + assertError(CoreRequestValidators.checkElement(element: element), .EMPTY_COLUMN_NAME_IN_COLLECT()) + } + + func testCheckElementUnmounted() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + // Not added to any window/view hierarchy: isMounted() is false. + + assertError(CoreRequestValidators.checkElement(element: element), .UNMOUNTED_COLLECT_ELEMENT(value: "name")) + } + + // MARK: - checkRevealElements / checkRevealElementsPreflight + + func testCheckRevealElementsValid() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "token1", label: "first")) + + XCTAssertNil(CoreRequestValidators.checkRevealElements(elements: [element])) + } + + func testCheckRevealElementsEmptyToken() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "", label: "first")) + + assertError(CoreRequestValidators.checkRevealElements(elements: [element]), .EMPTY_TOKEN_ID()) + } + + func testCheckRevealElementsErrorTriggered() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "token1", label: "first")) + element.setError("custom trigger message") + + assertError(CoreRequestValidators.checkRevealElements(elements: [element]), .ERROR_TRIGGERED(value: "custom trigger message")) + } + + func testCheckRevealElementsErrorTriggeredTakesPrecedenceOverEmptyToken() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "", label: "first")) + element.setError("custom trigger message") + + // errorTriggered is checked before the empty-token check for the same element. + assertError(CoreRequestValidators.checkRevealElements(elements: [element]), .ERROR_TRIGGERED(value: "custom trigger message")) + } + + func testCheckRevealElementsPreflightUnmounted() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "token1", label: "first")) + // Not mounted. + + assertError(CoreRequestValidators.checkRevealElementsPreflight(elements: [element]), .UNMOUNTED_REVEAL_ELEMENT(value: "token1")) + } + + func testCheckRevealElementsPreflightMountedValid() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "token1", label: "first")) + UIWindow().addSubview(element) + + XCTAssertNil(CoreRequestValidators.checkRevealElementsPreflight(elements: [element])) + } + + // MARK: - validateElementStates + + func testValidateElementStatesAllValid() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + UIWindow().addSubview(element) + + let result = CoreRequestValidators.validateElementStates(elements: [element]) + XCTAssertNil(result.errorCode) + XCTAssertEqual(result.errors, "") + } + + func testValidateElementStatesEmptyRequiredElement() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME), + options: CollectElementOptions(required: true)) + UIWindow().addSubview(element) + + let result = CoreRequestValidators.validateElementStates(elements: [element]) + XCTAssertNil(result.errorCode) + XCTAssertTrue(result.errors.contains("name is empty")) + } + + func testValidateElementStatesFailsFastOnElementError() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let unmounted = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + // Not mounted: checkElement fails before any per-element state accounting. + + let result = CoreRequestValidators.validateElementStates(elements: [unmounted]) + assertError(result.errorCode, .UNMOUNTED_COLLECT_ELEMENT(value: "name")) + XCTAssertEqual(result.errors, "") + } + + // MARK: - checkInsertRecordEntries + + func testCheckInsertRecordEntriesValid() { + let entries: [[String: Any]] = [["table": "persons", "fields": ["name": "john"]]] + XCTAssertNil(CoreRequestValidators.checkInsertRecordEntries(entries)) + } + + func testCheckInsertRecordEntriesMissingTableKey() { + let entries: [[String: Any]] = [["fields": ["name": "john"]]] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .TABLE_KEY_ERROR(value: "0")) + } + + func testCheckInsertRecordEntriesInvalidTableType() { + let entries: [[String: Any]] = [["table": 1, "fields": ["name": "john"]]] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .INVALID_TABLE_NAME_TYPE(value: "0")) + } + + func testCheckInsertRecordEntriesEmptyTableName() { + let entries: [[String: Any]] = [["table": "", "fields": ["name": "john"]]] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .EMPTY_TABLE_NAME()) + } + + func testCheckInsertRecordEntriesMissingFieldsKey() { + let entries: [[String: Any]] = [["table": "persons"]] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .FIELDS_KEY_ERROR(value: "0")) + } + + func testCheckInsertRecordEntriesInvalidFieldsType() { + let entries: [[String: Any]] = [["table": "persons", "fields": "not a dict"]] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .INVALID_FIELDS_TYPE(value: "0")) + } + + func testCheckInsertRecordEntriesEmptyFields() { + let entries: [[String: Any]] = [["table": "persons", "fields": [:] as [String: Any]]] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .EMPTY_FIELDS_KEY(value: "0")) + } + + func testCheckInsertRecordEntriesLastErrorWinsAcrossMultipleRecords() { + // Matches the original per-SDK loop's semantics: the loop doesn't break on most + // error kinds, so with multiple invalid records the *last* one's error wins. + let entries: [[String: Any]] = [ + ["fields": ["name": "john"]], // TABLE_KEY_ERROR at index 0 + ["table": "", "fields": ["name": "jane"]] // EMPTY_TABLE_NAME at index 1 -- wins + ] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .EMPTY_TABLE_NAME()) + } + + func testCheckInsertRecordEntriesInvalidFieldsTypeStopsScan() { + // INVALID_FIELDS_TYPE is the one branch that `break`s out of the loop, so a + // later record's error must not overwrite it. + let entries: [[String: Any]] = [ + ["table": "persons", "fields": "not a dict"], // INVALID_FIELDS_TYPE at index 0, then break + ["fields": ["name": "jane"]] // would be TABLE_KEY_ERROR at index 1 if reached + ] + assertError(CoreRequestValidators.checkInsertRecordEntries(entries), .INVALID_FIELDS_TYPE(value: "0")) + } +} diff --git a/Tests/skyflow-iOS-legacyTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-legacyTests/DemoImpl.swift rename to Tests/Skyflow/skyflow-iOS-legacyTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/MockURLProtocol.swift similarity index 100% rename from Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift rename to Tests/Skyflow/skyflow-iOS-legacyTests/MockURLProtocol.swift diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift similarity index 100% rename from Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift rename to Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift similarity index 100% rename from Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift rename to Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift new file mode 100644 index 00000000..666f4443 --- /dev/null +++ b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Validation-path tests for the legacy (v1) public Client operations +// (insert/detokenize) and the legacy initialize() SDK identity. + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_legacyClientApiTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration( + vaultID: "vault_id", + vaultURL: "https://example.org/", + tokenProvider: DemoTokenProvider(), + options: Options(logLevel: .DEBUG))) + } + + override func tearDown() { + skyflow = nil + } + + // MARK: - initialize() + + func testInitializeStampsLegacySdkName() { + // SDK_NAME is a process-wide global shared with the FlowVault SDK's + // tests; restore it so this test can't leak into other suites. + let originalSdkName = SDK_NAME + defer { SDK_NAME = originalSdkName } + + let client = Skyflow.initialize(Configuration( + vaultID: "vault_id", + vaultURL: "https://example.org/", + tokenProvider: DemoTokenProvider())) + + XCTAssertNotNil(client) + XCTAssertEqual(SDK_NAME, "skyflow-iOS") + } + + // MARK: - insert() validation (failures delivered as a raw NSError) + + func testInsertMissingRecordsKey() { + let expectation = XCTestExpectation(description: "insert without records key should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["norecords": []], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.RECORDS_KEY_ERROR().description) + } + + func testInsertInvalidRecordsType() { + let expectation = XCTestExpectation(description: "insert with non-array records should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": "not an array"], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_RECORDS_TYPE().description) + } + + func testInsertInvalidTableNameType() { + let expectation = XCTestExpectation(description: "insert with non-string table should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": [["table": 1, "fields": ["a": "b"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").description) + } + + func testInsertMissingTableKey() { + let expectation = XCTestExpectation(description: "insert with a record missing the table key should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": [["fields": ["a": "b"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_KEY_ERROR(value: "0").description) + } + + func testInsertEmptyTableName() { + let expectation = XCTestExpectation(description: "insert with an empty table name should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": [["table": "", "fields": ["a": "b"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_TABLE_NAME().description) + } + + func testInsertMissingFieldsKey() { + let expectation = XCTestExpectation(description: "insert with a record missing the fields key should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": [["table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.FIELDS_KEY_ERROR(value: "0").description) + } + + func testInsertInvalidFieldsType() { + let expectation = XCTestExpectation(description: "insert with a non-dictionary fields value should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": [["table": "persons", "fields": "not a dict"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_FIELDS_TYPE(value: "0").description) + } + + func testInsertEmptyFields() { + let expectation = XCTestExpectation(description: "insert with an empty fields dictionary should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.insert(records: ["records": [["table": "persons", "fields": [:] as [String: Any]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").description) + } + + func testInsertEmptyUpsertOptions() { + let expectation = XCTestExpectation(description: "insert with empty upsert array should fail") + let callback = DemoAPICallback(expectation: expectation) + let records: [String: Any] = ["records": [["table": "persons", "fields": ["name": "john"]]]] + + skyflow.insert(records: records, options: InsertOptions(tokens: true, upsert: []), callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().description) + } + + func testInsertEmptyVaultURL() { + let expectation = XCTestExpectation(description: "insert with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let client = Client(Configuration(vaultID: "vault_id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + client.insert(records: ["records": [["table": "persons", "fields": ["name": "john"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().description) + } + + // MARK: - detokenize() validation (failures wrapped as {"errors": [NSError]}) + + private func detokenizeError(records: [String: Any]) -> NSError? { + let expectation = XCTestExpectation(description: "detokenize should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.detokenize(records: records, callback: callback) + + wait(for: [expectation], timeout: 10.0) + return (callback.data["errors"] as? [NSError])?.first + } + + func testDetokenizeMissingRecordsKey() { + let error = detokenizeError(records: ["norecords": []]) + + XCTAssertEqual(error?.localizedDescription, ErrorCodes.RECORDS_KEY_ERROR().description) + } + + func testDetokenizeEmptyRecords() { + let error = detokenizeError(records: ["records": [] as [[String: Any]]]) + + XCTAssertEqual(error?.localizedDescription, ErrorCodes.EMPTY_RECORDS_OBJECT().description) + } + + func testDetokenizeInvalidRecordsType() { + let error = detokenizeError(records: ["records": "not an array"]) + + XCTAssertEqual(error?.localizedDescription, ErrorCodes.INVALID_RECORDS_TYPE().description) + } + + func testDetokenizeMissingToken() { + let error = detokenizeError(records: ["records": [["redaction": RedactionType.PLAIN_TEXT]]]) + + XCTAssertEqual(error?.localizedDescription, ErrorCodes.ID_KEY_ERROR().description) + } + + func testDetokenizeInvalidTokenType() { + let error = detokenizeError(records: ["records": [["token": 123]]]) + + XCTAssertEqual(error?.localizedDescription, ErrorCodes.INVALID_TOKEN_TYPE(value: "0").description) + } + + func testDetokenizeInvalidRedactionType() { + // Legacy detokenize takes redaction as a RedactionType enum value; a raw string must be rejected. + let error = detokenizeError(records: ["records": [["token": "abc", "redaction": "PLAIN_TEXT"]]]) + + XCTAssertEqual(error?.localizedDescription, ErrorCodes.INVALID_REDACTION_TYPE().description) + } + + func testDetokenizeEmptyVaultID() { + let expectation = XCTestExpectation(description: "detokenize with empty vaultID should fail") + let callback = DemoAPICallback(expectation: expectation) + let client = Client(Configuration(vaultID: "", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider())) + + client.detokenize(records: ["records": [["token": "abc"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let error = (callback.data["errors"] as? [NSError])?.first + XCTAssertEqual(error?.localizedDescription, ErrorCodes.EMPTY_VAULT_ID().description) + } + + // MARK: - Full request flows (valid input; the unresolvable vault host fails fast + // at the network layer after the whole request-building path has executed) + + private func offlineClient() -> Client { + return Client(Configuration(vaultID: "vault_id", vaultURL: "https://testvault.skyflow.invalid/", + tokenProvider: DemoTokenProvider())) + } + + func testInsertValidRecordsRunsFullRequestFlow() { + let expectation = XCTestExpectation(description: "insert should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + + offlineClient().insert(records: ["records": [["table": "persons", "fields": ["name": "john"]]]], + callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertFalse(callback.receivedResponse.isEmpty && callback.data.isEmpty) + } + + func testDetokenizeValidRecordsWithExplicitRedactionRunsFullRequestFlow() { + let expectation = XCTestExpectation(description: "detokenize should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + + offlineClient().detokenize(records: ["records": [ + ["token": "tok1", "redaction": RedactionType.MASKED], + ["token": "tok2"] + ]], callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + // MARK: - getById() validation branches + + private func getByIdError(records: [String: Any]) -> NSError? { + let expectation = XCTestExpectation(description: "getById should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.getById(records: records, callback: callback) + + wait(for: [expectation], timeout: 10.0) + return (callback.data["errors"] as? [NSError])?.first + } + + func testGetByIdMissingRecordsKey() { + XCTAssertEqual(getByIdError(records: ["norecords": []])?.localizedDescription, + ErrorCodes.EMPTY_RECORDS_OBJECT().description) + } + + func testGetByIdEmptyRecords() { + XCTAssertEqual(getByIdError(records: ["records": [] as [[String: Any]]])?.localizedDescription, + ErrorCodes.EMPTY_RECORDS_OBJECT().description) + } + + func testGetByIdInvalidRecordsType() { + XCTAssertEqual(getByIdError(records: ["records": "not an array"])?.localizedDescription, + ErrorCodes.INVALID_RECORDS_TYPE().description) + } + + func testGetByIdInvalidEntryReportsValidatorError() { + XCTAssertEqual(getByIdError(records: ["records": [["table": "persons", "redaction": RedactionType.PLAIN_TEXT]]])?.localizedDescription, + ErrorCodes.MISSING_KEY_IDS(value: "0").description) + } + + func testGetByIdValidRecordsRunsFullRequestFlow() { + let expectation = XCTestExpectation(description: "getById should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + + offlineClient().getById(records: ["records": [ + ["ids": ["id1", "id2"], "table": "persons", "redaction": RedactionType.PLAIN_TEXT] + ]], callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + // MARK: - get() validation branches + + private func getError(records: [String: Any], options: GetOptions = GetOptions()) -> NSError? { + let expectation = XCTestExpectation(description: "get should fail") + let callback = DemoAPICallback(expectation: expectation) + + skyflow.get(records: records, options: options, callback: callback) + + wait(for: [expectation], timeout: 10.0) + return (callback.data["errors"] as? [NSError])?.first + } + + func testGetMissingRecordsKey() { + XCTAssertEqual(getError(records: ["norecords": []])?.localizedDescription, + ErrorCodes.EMPTY_RECORDS_OBJECT().description) + } + + func testGetEmptyRecords() { + XCTAssertEqual(getError(records: ["records": [] as [[String: Any]]])?.localizedDescription, + ErrorCodes.EMPTY_RECORDS_OBJECT().description) + } + + func testGetInvalidRecordsType() { + XCTAssertEqual(getError(records: ["records": "not an array"])?.localizedDescription, + ErrorCodes.INVALID_RECORDS_TYPE().description) + } + + func testGetValidIdsWithRedactionRunsFullRequestFlow() { + let expectation = XCTestExpectation(description: "get by ids should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + + offlineClient().get(records: ["records": [ + ["ids": ["id1"], "table": "persons", "redaction": RedactionType.PLAIN_TEXT] + ]], callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testGetValidIdsWithTokensOptionRunsFullRequestFlow() { + let expectation = XCTestExpectation(description: "get with tokens should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + + offlineClient().get(records: ["records": [["ids": ["id1"], "table": "persons"]]], + options: GetOptions(tokens: true), callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testGetValidColumnValuesRunsFullRequestFlow() { + let expectation = XCTestExpectation(description: "get by column values should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + + offlineClient().get(records: ["records": [ + ["table": "persons", "columnName": "email", "columnValues": ["a@b.com"], "redaction": RedactionType.PLAIN_TEXT] + ]], callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + // MARK: - createDetokenizeRecords + + func testCreateDetokenizeRecordsBuildsTokenList() { + let records = skyflow.createDetokenizeRecords(["id1": "tok1"]) + + XCTAssertEqual(records["records"], [["token": "tok1"]]) + } +} diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift new file mode 100644 index 00000000..25de677d --- /dev/null +++ b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Branch coverage for CollectAPICallback: the onSuccess insert/update +// orchestration (driven with unresolvable/invalid URLs so no real vault is +// needed) and the response-processing helpers called directly with +// constructed HTTPURLResponse + Data. + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_legacyCollectApiCallbackCoverageTests: XCTestCase { + + private let insertRecords: [String: Any] = ["records": [["table": "persons", "fields": ["name": "john"]]]] + + private func makeCallback(records: [String: Any], vaultURL: String, + tokens: Bool = false, clientCallback: Callback) -> CollectAPICallback { + return CollectAPICallback( + callback: clientCallback, + apiClient: APIClient(vaultID: "vault_id", vaultURL: vaultURL, tokenProvider: DemoTokenProvider()), + records: records, + options: ICOptions(tokens: tokens), + contextOptions: ContextOptions()) + } + + private func response(status: Int, headers: [String: String]? = nil) -> HTTPURLResponse { + return HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: status, + httpVersion: nil, headerFields: headers)! + } + + // MARK: - onSuccess orchestration + + func testOnSuccessInsertWithMalformedVaultURLReportsErrors() { + // iOS 17+ URL(string:) percent-encodes the invalid characters, so the request + // proceeds and fails at the network layer; the failure still surfaces as errors. + let expectation = XCTestExpectation(description: "malformed vault URL should fail") + let callback = DemoAPICallback(expectation: expectation) + let apiCallback = makeCallback(records: insertRecords, vaultURL: "invalid url", clientCallback: callback) + + apiCallback.onSuccess("token") + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testOnSuccessInsertWithUnreachableHostMergesIntoErrors() { + let expectation = XCTestExpectation(description: "unreachable host should merge into errors") + let callback = DemoAPICallback(expectation: expectation) + let apiCallback = makeCallback(records: insertRecords, vaultURL: "https://vault.skyflow.invalid/", + clientCallback: callback) + + apiCallback.onSuccess("token") + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testOnSuccessUpdateWithUnreachableHostMergesIntoErrors() { + let records: [String: Any] = ["update": [ + "key1": ["table": "persons", "skyflowID": "sid-1", "fields": ["name": "john"]] + ]] + let expectation = XCTestExpectation(description: "unreachable update host should merge into errors") + let callback = DemoAPICallback(expectation: expectation) + let apiCallback = makeCallback(records: records, vaultURL: "https://vault.skyflow.invalid/", + clientCallback: callback) + + apiCallback.onSuccess("token") + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testOnSuccessUpdateRecordMissingSkyflowIdIsSkipped() { + let records: [String: Any] = ["update": ["key1": ["table": "persons"]]] + let expectation = XCTestExpectation(description: "update entry without skyflowID should be skipped") + let callback = DemoAPICallback(expectation: expectation) + let apiCallback = makeCallback(records: records, vaultURL: "https://vault.skyflow.invalid/", + clientCallback: callback) + + apiCallback.onSuccess("token") + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual((callback.data["errors"] as? [Any])?.count, 0) + } + + func testOnSuccessUpdateRecordWithUnencodableTableReportsInvalidUrl() { + let records: [String: Any] = ["update": [ + "key1": ["table": "bad table", "skyflowID": "sid 1", "fields": ["name": "john"]] + ]] + let expectation = XCTestExpectation(description: "update entry producing an invalid URL should error") + let callback = DemoAPICallback(expectation: expectation) + let apiCallback = makeCallback(records: records, vaultURL: "https://vault.skyflow.invalid/", + clientCallback: callback) + + apiCallback.onSuccess("token") + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual((callback.data["errors"] as? [Any])?.count, 1) + } + + // MARK: - processUpdateResponse + + private var directCallback: CollectAPICallback { + makeCallback(records: insertRecords, vaultURL: "https://example.org/", + clientCallback: DemoAPICallback(expectation: XCTestExpectation())) + } + + func testProcessUpdateResponseWithTokensBuildsFieldsRecord() throws { + let apiCallback = makeCallback(records: insertRecords, vaultURL: "https://example.org/", + tokens: true, + clientCallback: DemoAPICallback(expectation: XCTestExpectation())) + let data = try JSONSerialization.data(withJSONObject: ["skyflow_id": "sid-1", "tokens": ["card": "tok-1"]]) + + let result = try apiCallback.processUpdateResponse(data: data, response: response(status: 200), + error: nil, table: "persons") + + let record = (result["records"] as! [[String: Any]])[0] + let fields = record["fields"] as! [String: Any] + XCTAssertEqual(fields["card"] as? String, "tok-1") + XCTAssertEqual(fields["skyflow_id"] as? String, "sid-1") + XCTAssertEqual(record["table"] as? String, "persons") + } + + func testProcessUpdateResponseWithoutTokensAndNumericSkyflowId() throws { + let data = try JSONSerialization.data(withJSONObject: ["skyflow_id": 123]) + + let result = try directCallback.processUpdateResponse(data: data, response: response(status: 200), + error: nil, table: "persons") + + let record = (result["records"] as! [[String: Any]])[0] + XCTAssertEqual(record["skyflow_id"] as? String, "123") + } + + func testProcessUpdateResponseWithNilDataReturnsEmptyRecords() throws { + let result = try directCallback.processUpdateResponse(data: nil, response: response(status: 200), + error: nil, table: "persons") + + XCTAssertEqual((result["records"] as? [Any])?.count, 0) + } + + func testProcessUpdateResponseBadStatusIncludesRequestId() throws { + let data = try JSONSerialization.data(withJSONObject: ["error": ["message": "update failed"]]) + + let result = try directCallback.processUpdateResponse( + data: data, response: response(status: 400, headers: ["x-request-id": "rid-1"]), + error: nil, table: "persons") + + let error = result["error"] as! [String: Any] + XCTAssertTrue((error["message"] as! String).contains("update failed")) + XCTAssertTrue((error["message"] as! String).contains("rid-1")) + XCTAssertEqual(error["code"] as? Int, 400) + } + + func testProcessUpdateResponseBadStatusWithNonJsonBody() throws { + let result = try directCallback.processUpdateResponse( + data: Data("not json".utf8), response: response(status: 500), error: nil, table: "persons") + + let error = result["error"] as! [String: Any] + XCTAssertEqual(error["message"] as? String, "not json") + XCTAssertEqual(error["code"] as? Int, 500) + } + + // MARK: - processResponse / getCollectResponseBody + + func testProcessResponseBadStatusWithNonJsonBody() throws { + let result = try directCallback.processResponse(data: Data("boom".utf8), + response: response(status: 502), error: nil) + + let error = result["error"] as! [String: Any] + XCTAssertEqual(error["message"] as? String, "boom") + XCTAssertEqual(error["code"] as? Int, 502) + } + + func testProcessResponseWithNilDataReturnsEmptyDictionary() throws { + let result = try directCallback.processResponse(data: nil, response: response(status: 200), error: nil) + + XCTAssertTrue(result.isEmpty) + } + + func testProcessResponseSuccessParsesBatchBody() throws { + let body: [String: Any] = ["responses": [["records": [["skyflow_id": "sid-1"]]]]] + let data = try JSONSerialization.data(withJSONObject: body) + + let result = try directCallback.processResponse(data: data, response: response(status: 200), error: nil) + + let record = (result["records"] as! [[String: Any]])[0] + XCTAssertEqual(record["skyflow_id"] as? String, "sid-1") + XCTAssertEqual(record["table"] as? String, "persons") + } + + func testGetCollectResponseBodyWithTokensMapsFieldsAndSkyflowId() throws { + let apiCallback = makeCallback(records: insertRecords, vaultURL: "https://example.org/", + tokens: true, + clientCallback: DemoAPICallback(expectation: XCTestExpectation())) + let body: [String: Any] = ["responses": [ + ["records": [["skyflow_id": "sid-1"]]], + ["fields": ["card": "tok-1"]] + ]] + let data = try JSONSerialization.data(withJSONObject: body) + + let result = try apiCallback.getCollectResponseBody(data: data) + + let record = (result["records"] as! [[String: Any]])[0] + let fields = record["fields"] as! [String: Any] + XCTAssertEqual(fields["card"] as? String, "tok-1") + XCTAssertEqual(fields["skyflow_id"] as? String, "sid-1") + } +} diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift new file mode 100644 index 00000000..e864fe4d --- /dev/null +++ b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Tests for the legacy (v1) collect layer: container collect() / +// composable collect() validation paths and the legacy option/input models +// (untyped additionalFields + upsert dictionaries, tokens flag). + +import UIKit +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_legacyCollectOpsTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration( + vaultID: "vault_id", + vaultURL: "https://example.org/", + tokenProvider: DemoTokenProvider(), + options: Options(logLevel: .DEBUG))) + } + + override func tearDown() { + skyflow = nil + } + + // MARK: - Legacy option models + + func testCollectOptionsDefaults() { + let options = CollectOptions() + + XCTAssertTrue(options.tokens) + XCTAssertNil(options.additionalFields) + XCTAssertNil(options.upsert) + } + + func testInsertOptionsDefaults() { + let options = InsertOptions() + + XCTAssertTrue(options.tokens) + XCTAssertNil(options.upsert) + } + + func testGetOptionsDefaults() { + XCTAssertFalse(GetOptions().tokens) + XCTAssertTrue(GetOptions(tokens: true).tokens) + } + + func testCollectElementInputLegacyArgumentLabels() { + // The legacy contract uses `table:` and `skyflowID:` labels (vs FlowVault's + // `tableName:`/`skyflowId:`), mapped onto the shared base input. + let input = CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME, skyflowID: "sid-1") + + XCTAssertEqual(input.data.tableName, "persons") + XCTAssertEqual(input.data.column, "name") + XCTAssertEqual(input.data.skyflowId, "sid-1") + } + + // MARK: - Container collect() validation + + func testCollectEmptyVaultURL() { + let client = Client(Configuration(vaultID: "vault_id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let container = client.container(type: ContainerType.COLLECT, options: nil) + + let expectation = XCTestExpectation(description: "collect with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().description) + } + + func testCollectAdditionalFieldsMissingRecordsKey() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + + let expectation = XCTestExpectation(description: "collect with malformed additionalFields should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: CollectOptions(additionalFields: ["norecords": []])) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_RECORDS_IN_ADDITIONAL_FIELDS().description) + } + + func testCollectEmptyUpsertOptions() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + + let expectation = XCTestExpectation(description: "collect with empty upsert array should fail") + let callback = DemoAPICallback(expectation: expectation) + let additionalFields: [String: Any] = ["records": [["table": "persons", "fields": ["name": "john"]]]] + container!.collect(callback: callback, options: CollectOptions(additionalFields: additionalFields, upsert: [])) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().description) + } + + func testCollectUpsertMissingColumn() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + + let expectation = XCTestExpectation(description: "collect with column-less upsert should fail") + let callback = DemoAPICallback(expectation: expectation) + let additionalFields: [String: Any] = ["records": [["table": "persons", "fields": ["name": "john"]]]] + container!.collect(callback: callback, options: CollectOptions(additionalFields: additionalFields, upsert: [["table": "persons"]])) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").description) + } + + // MARK: - Composable collect() validation + + func testComposableCollectEmptyVaultURL() { + let client = Client(Configuration(vaultID: "vault_id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let container = client.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + + let expectation = XCTestExpectation(description: "composable collect with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().description) + } + + func testComposableCollectAdditionalFieldsMissingRecordsKey() { + let container = skyflow.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + + let expectation = XCTestExpectation(description: "composable collect with malformed additionalFields should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: CollectOptions(additionalFields: ["norecords": []])) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_RECORDS_IN_ADDITIONAL_FIELDS().description) + } + + // MARK: - collect()/composable collect() element-state branches and full flow + + // DemoTokenProvider's "dummy_token" isn't JWT-shaped, so isTokenValid() would reject + // it and short-circuit the request with INVALID_BEARER_TOKEN_FORMAT before ever + // reaching the network layer. offlineClient() needs a well-formed (if unverified) + // token so requests actually get dispatched and fail at the DNS/network layer instead. + private class ValidFormatTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + let payload = try! JSONSerialization.data(withJSONObject: ["exp": 9_999_999_999]) + apiCallback.onSuccess("header.\(payload.base64EncodedString()).signature") + } + } + + // Client whose vault host cannot resolve, so full-flow tests fail fast at the + // network layer after exercising the entire request-building path. + private func offlineClient() -> Client { + return Client(Configuration(vaultID: "vault_id", vaultURL: "https://testvault.skyflow.invalid/", + tokenProvider: ValidFormatTokenProvider())) + } + + func testCollectUnmountedElementFails() { + let client = offlineClient() + let container = client.container(type: ContainerType.COLLECT, options: nil) + _ = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + + let expectation = XCTestExpectation(description: "collect with unmounted element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNMOUNTED_COLLECT_ELEMENT(value: "name").description) + } + + func testCollectEmptyRequiredElementReportsValidationErrors() { + let client = offlineClient() + let container = client.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME), + options: CollectElementOptions(required: true)) + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "collect with empty required element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertTrue(callback.receivedResponse.contains("name is empty")) + } + + func testCollectValidElementRunsFullRequestFlow() { + let client = offlineClient() + let container = client.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + element.textField.secureText = "John" + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "collect should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: CollectOptions(upsert: [["table": "persons", "column": "name"]])) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testComposableCreateAndCollectUnmountedElementFails() { + let client = offlineClient() + let container = client.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + _ = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + + let expectation = XCTestExpectation(description: "composable collect with unmounted element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNMOUNTED_COLLECT_ELEMENT(value: "name").description) + } + + func testComposableCollectEmptyRequiredElementReportsValidationErrors() { + let client = offlineClient() + let container = client.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME), + options: CollectElementOptions(required: true)) + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "composable collect with empty required element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertTrue(callback.receivedResponse.contains("name is empty")) + } + + func testComposableCollectValidElementRunsFullRequestFlow() { + let client = offlineClient() + let container = client.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + element.textField.secureText = "John" + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "composable collect should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: CollectOptions(upsert: [["table": "persons", "column": "name"]])) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + // MARK: - Legacy model init overloads and update extensions + + func testCollectElementInputInitWithoutType() { + let input = CollectElementInput(table: "persons", column: "name") + + XCTAssertEqual(input.data.tableName, "persons") + XCTAssertEqual(input.data.column, "name") + XCTAssertNil(input.data.type) + } + + @available(*, deprecated) // silences the warning for exercising the deprecated altText: initializer + func testCollectElementInputDeprecatedAltTextInit() { + let input = CollectElementInput(table: "persons", column: "name", altText: "alt", type: .CARDHOLDER_NAME) + + XCTAssertEqual(input.data.tableName, "persons") + XCTAssertEqual(input.data.column, "name") + } + + func testTextFieldUpdateWithInputAndOptions() { + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let element = container!.create(input: CollectElementInput(table: "persons", column: "name", type: .CARDHOLDER_NAME)) + + element.update(update: CollectElementInput(table: "persons", column: "last_name", type: .CARDHOLDER_NAME)) + element.update(updateOptions: CollectElementOptions(required: true)) + + XCTAssertEqual(element.collectInput.column, "last_name") + } + + func testContainerOptionsEmptyInit() { + let options = ContainerOptions() + + XCTAssertNotNil(options.data) + } +} diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift new file mode 100644 index 00000000..3e78e46c --- /dev/null +++ b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Tests for the legacy (v1) reveal layer: per-token redaction on +// RevealElementInput, RevealRequestBody, RevealValueCallback and the +// container reveal() validation paths. + +import UIKit +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_legacyRevealOpsTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration( + vaultID: "vault_id", + vaultURL: "https://example.org/", + tokenProvider: DemoTokenProvider(), + options: Options(logLevel: .DEBUG))) + } + + override func tearDown() { + skyflow = nil + } + + private func waitForUIUpdates() { + let expectation = self.expectation(description: "main queue drained") + DispatchQueue.main.async { + expectation.fulfill() + } + self.waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - RevealElementInput (legacy per-token redaction) + + func testRevealElementInputDefaultsToPlainTextRedaction() { + let input = RevealElementInput(token: "token1", label: "label") + + XCTAssertEqual(input.data.redaction, RedactionType.PLAIN_TEXT) + } + + func testRevealElementInputCarriesExplicitRedaction() { + let input = RevealElementInput(token: "token1", label: "label", redaction: .MASKED) + + XCTAssertEqual(input.data.redaction, RedactionType.MASKED) + } + + // MARK: - RevealRequestBody + + func testCreateRevealRequestBodyCarriesPerTokenRedaction() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let masked = container!.create(input: RevealElementInput(token: "token1", label: "first", redaction: .MASKED)) + let plain = container!.create(input: RevealElementInput(token: "token2", label: "second")) + + let body = RevealRequestBody.createRequestBody(elements: [masked, plain]) + let records = body["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["token"] as? String, "token1") + XCTAssertEqual(records[0]["redaction"] as? RedactionType, .MASKED) + XCTAssertEqual(records[1]["token"] as? String, "token2") + XCTAssertEqual(records[1]["redaction"] as? RedactionType, .PLAIN_TEXT) + } + + // MARK: - RevealValueCallback + + func testRevealValueCallbackOnSuccessUpdatesElementsAndReportsErrors() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let revealed = container!.create(input: RevealElementInput(token: "token1", label: "first")) + let failed = container!.create(input: RevealElementInput(token: "token2", label: "second")) + + let expectation = XCTestExpectation(description: "client callback should receive merged response") + let clientCallback = DemoAPICallback(expectation: expectation) + let revealValueCallback = RevealValueCallback( + callback: clientCallback, + revealElements: [revealed, failed], + contextOptions: ContextOptions()) + + revealValueCallback.onSuccess([ + "records": [["token": "token1", "value": "John"]], + "errors": [["token": "token2", "error": "Invalid Token"]] + ] as [String: Any]) + + wait(for: [expectation], timeout: 10.0) + waitForUIUpdates() + + let success = clientCallback.data["success"] as! [[String: String]] + XCTAssertEqual(success, [["token": "token1"]]) + let errors = clientCallback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["token"] as? String, "token2") + + XCTAssertEqual(revealed.actualValue, "John") + XCTAssertNil(revealed.errorMessage.text) + XCTAssertNil(failed.actualValue) + XCTAssertEqual(failed.errorMessage.text, "Invalid Token") + } + + func testRevealValueCallbackOnFailureRoutesErrorsToClientOnFailure() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let failed = container!.create(input: RevealElementInput(token: "token1", label: "first")) + + let expectation = XCTestExpectation(description: "client callback should receive failure response") + let clientCallback = DemoAPICallback(expectation: expectation) + let revealValueCallback = RevealValueCallback( + callback: clientCallback, + revealElements: [failed], + contextOptions: ContextOptions()) + + revealValueCallback.onFailure([ + "errors": [["token": "token1", "error": "Invalid Token"]] + ] as [String: Any]) + + wait(for: [expectation], timeout: 10.0) + waitForUIUpdates() + + let errors = clientCallback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["token"] as? String, "token1") + XCTAssertEqual(failed.errorMessage.text, "Invalid Token") + } + + func testRevealValueCallbackOnFailureWithNonDictionaryDoesNotCrash() { + let expectation = XCTestExpectation(description: "client callback should still be invoked") + let clientCallback = DemoAPICallback(expectation: expectation) + let revealValueCallback = RevealValueCallback( + callback: clientCallback, + revealElements: [], + contextOptions: ContextOptions()) + + revealValueCallback.onFailure("not a dictionary") + + wait(for: [expectation], timeout: 10.0) + XCTAssertNil(clientCallback.data["success"]) + XCTAssertNil(clientCallback.data["errors"]) + } + + // MARK: - Container reveal() validation + + func testRevealEmptyVaultURL() { + let client = Client(Configuration(vaultID: "vault_id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let container = client.container(type: ContainerType.REVEAL, options: nil) + _ = container!.create(input: RevealElementInput(token: "token1", label: "first")) + + let expectation = XCTestExpectation(description: "reveal with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.reveal(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().description) + } + + func testRevealUnmountedElement() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + _ = container!.create(input: RevealElementInput(token: "token1", label: "first")) + + let expectation = XCTestExpectation(description: "reveal with unmounted element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.reveal(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNMOUNTED_REVEAL_ELEMENT(value: "token1").description) + } + + func testRevealElementWithTriggeredErrorFails() { + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "token1", label: "first")) + UIWindow().addSubview(element) + element.setError("manually triggered error") + + let expectation = XCTestExpectation(description: "reveal with an error-triggered element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.reveal(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.ERROR_TRIGGERED(value: "manually triggered error").description) + } + + func testRevealMountedElementRunsFullRequestFlow() { + // Unresolvable vault host: the full reveal path (request body, per-token + // redaction list, apiClient.get) executes and fails fast at the network layer. + let client = Client(Configuration(vaultID: "vault_id", vaultURL: "https://testvault.skyflow.invalid/", + tokenProvider: DemoTokenProvider())) + let container = client.container(type: ContainerType.REVEAL, options: nil) + let element = container!.create(input: RevealElementInput(token: "token1", label: "first", redaction: .MASKED)) + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "reveal should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + container!.reveal(callback: callback) + + wait(for: [expectation], timeout: 20.0) + XCTAssertNotNil(callback.data["errors"]) + } + + func testRevealValueCallbackOnFailureWithRecordsUpdatesElements() { + // A partial-failure payload delivered through onFailure: revealed values still + // update their elements while token-scoped errors surface inline. + let container = skyflow.container(type: ContainerType.REVEAL, options: nil) + let revealed = container!.create(input: RevealElementInput(token: "token1", label: "first")) + let failed = container!.create(input: RevealElementInput(token: "token2", label: "second")) + + let expectation = XCTestExpectation(description: "client callback should receive failure response") + let clientCallback = DemoAPICallback(expectation: expectation) + let revealValueCallback = RevealValueCallback( + callback: clientCallback, + revealElements: [revealed, failed], + contextOptions: ContextOptions()) + + revealValueCallback.onFailure([ + "records": [["token": "token1", "value": "John"]], + "errors": [["token": "token2", "error": "Invalid Token"]] + ] as [String: Any]) + + wait(for: [expectation], timeout: 10.0) + waitForUIUpdates() + + let success = clientCallback.data["success"] as! [[String: String]] + XCTAssertEqual(success, [["token": "token1"]]) + XCTAssertEqual((clientCallback.data["errors"] as! [[String: Any]]).count, 1) + XCTAssertEqual(revealed.actualValue, "John") + XCTAssertEqual(failed.errorMessage.text, "Invalid Token") + } +} diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift similarity index 100% rename from Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift rename to Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift new file mode 100644 index 00000000..cd3620c1 --- /dev/null +++ b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Direct tests for the legacy (v1) RequestValidators: checkRecord, +// checkAdditionalFields, checkDetokenizeRecord and checkGetByIdEntry. +// (checkUpsertOptions is covered in skyflow_iOS_legacyUtilTests, and +// validateGetRecords in skyflow-iOS-getTests.) + +import XCTest +@testable import Skyflow +@testable import SkyflowCore + +final class skyflow_iOS_legacyValidatorTests: XCTestCase { + + private func assertError(_ actual: ErrorCodes?, _ expected: ErrorCodes) { + XCTAssertEqual(actual?.getErrorObject(contextOptions: ContextOptions()).localizedDescription, + expected.getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + // MARK: - checkRecord + + func testCheckRecordValid() { + XCTAssertNil(RequestValidators.checkRecord(record: ["table": "persons", "fields": ["name": "john"]], index: 0)) + } + + func testCheckRecordMissingTable() { + assertError(RequestValidators.checkRecord(record: ["fields": ["name": "john"]], index: 0), + .TABLE_KEY_ERROR(value: "0")) + } + + func testCheckRecordInvalidTableType() { + assertError(RequestValidators.checkRecord(record: ["table": 1, "fields": ["name": "john"]], index: 0), + .INVALID_TABLE_NAME_TYPE(value: "0")) + } + + func testCheckRecordEmptyTableName() { + assertError(RequestValidators.checkRecord(record: ["table": "", "fields": ["name": "john"]], index: 0), + .EMPTY_TABLE_NAME()) + } + + func testCheckRecordMissingFields() { + assertError(RequestValidators.checkRecord(record: ["table": "persons"], index: 0), + .FIELDS_KEY_ERROR(value: "0")) + } + + func testCheckRecordInvalidFieldsType() { + assertError(RequestValidators.checkRecord(record: ["table": "persons", "fields": "not a dict"], index: 0), + .INVALID_FIELDS_TYPE(value: "0")) + } + + func testCheckRecordEmptyFields() { + assertError(RequestValidators.checkRecord(record: ["table": "persons", "fields": [:] as [String: Any]], index: 0), + .EMPTY_FIELDS_KEY(value: "0")) + } + + // MARK: - checkAdditionalFields + + func testCheckAdditionalFieldsValid() { + let additionalFields: [String: Any] = ["records": [["table": "persons", "fields": ["name": "john"]]]] + + XCTAssertNil(RequestValidators.checkAdditionalFields(additionalFields)) + } + + func testCheckAdditionalFieldsMissingRecordsKey() { + assertError(RequestValidators.checkAdditionalFields(["norecords": []]), + .MISSING_RECORDS_IN_ADDITIONAL_FIELDS()) + } + + func testCheckAdditionalFieldsEmptyRecords() { + assertError(RequestValidators.checkAdditionalFields(["records": [] as [[String: Any]]]), + .EMPTY_RECORDS_OBJECT()) + } + + func testCheckAdditionalFieldsInvalidRecordsType() { + assertError(RequestValidators.checkAdditionalFields(["records": "not an array"]), + .INVALID_RECORDS_TYPE()) + } + + func testCheckAdditionalFieldsPropagatesRecordErrorWithIndex() { + let additionalFields: [String: Any] = ["records": [ + ["table": "persons", "fields": ["name": "john"]], + ["fields": ["name": "jane"]] + ]] + + assertError(RequestValidators.checkAdditionalFields(additionalFields), + .TABLE_KEY_ERROR(value: "1")) + } + + // MARK: - checkDetokenizeRecord + + func testCheckDetokenizeRecordValidWithoutRedaction() { + XCTAssertNil(RequestValidators.checkDetokenizeRecord(token: ["token": "abc"], index: 0)) + } + + func testCheckDetokenizeRecordValidWithRedaction() { + XCTAssertNil(RequestValidators.checkDetokenizeRecord(token: ["token": "abc", "redaction": RedactionType.MASKED], index: 0)) + } + + func testCheckDetokenizeRecordInvalidRedactionType() { + assertError(RequestValidators.checkDetokenizeRecord(token: ["token": "abc", "redaction": "MASKED"], index: 0), + .INVALID_REDACTION_TYPE()) + } + + func testCheckDetokenizeRecordMissingToken() { + assertError(RequestValidators.checkDetokenizeRecord(token: ["redaction": RedactionType.PLAIN_TEXT], index: 0), + .ID_KEY_ERROR()) + } + + func testCheckDetokenizeRecordInvalidTokenType() { + assertError(RequestValidators.checkDetokenizeRecord(token: ["token": 123], index: 2), + .INVALID_TOKEN_TYPE(value: "2")) + } + + // MARK: - checkGetByIdEntry + + private var validGetByIdEntry: [String: Any] { + ["ids": ["id1"], "table": "persons", "redaction": RedactionType.PLAIN_TEXT] + } + + func testCheckGetByIdEntryValid() { + XCTAssertNil(RequestValidators.checkGetByIdEntry(entry: validGetByIdEntry, index: 0)) + } + + func testCheckGetByIdEntryEmpty() { + assertError(RequestValidators.checkGetByIdEntry(entry: [:], index: 0), + .EMPTY_RECORDS_OBJECT()) + } + + func testCheckGetByIdEntryMissingIds() { + assertError(RequestValidators.checkGetByIdEntry(entry: ["table": "persons", "redaction": RedactionType.PLAIN_TEXT], index: 0), + .MISSING_KEY_IDS(value: "0")) + } + + func testCheckGetByIdEntryInvalidIdsType() { + var entry = validGetByIdEntry + entry["ids"] = "not an array" + + assertError(RequestValidators.checkGetByIdEntry(entry: entry, index: 0), .INVALID_IDS_TYPE()) + } + + func testCheckGetByIdEntryEmptyIdsArray() { + var entry = validGetByIdEntry + entry["ids"] = [] as [String] + + assertError(RequestValidators.checkGetByIdEntry(entry: entry, index: 0), .EMPTY_IDS(value: "0")) + } + + func testCheckGetByIdEntryEmptyIdValue() { + var entry = validGetByIdEntry + entry["ids"] = ["id1", ""] + + assertError(RequestValidators.checkGetByIdEntry(entry: entry, index: 0), .EMPTY_ID_VALUE(value: "0")) + } + + func testCheckGetByIdEntryMissingTable() { + assertError(RequestValidators.checkGetByIdEntry(entry: ["ids": ["id1"], "redaction": RedactionType.PLAIN_TEXT], index: 0), + .TABLE_KEY_ERROR(value: "0")) + } + + func testCheckGetByIdEntryInvalidTableType() { + var entry = validGetByIdEntry + entry["table"] = 5 + + assertError(RequestValidators.checkGetByIdEntry(entry: entry, index: 0), .INVALID_TABLE_NAME_TYPE(value: "0")) + } + + func testCheckGetByIdEntryEmptyTableName() { + var entry = validGetByIdEntry + entry["table"] = "" + + assertError(RequestValidators.checkGetByIdEntry(entry: entry, index: 0), .EMPTY_TABLE_NAME()) + } + + func testCheckGetByIdEntryMissingRedaction() { + assertError(RequestValidators.checkGetByIdEntry(entry: ["ids": ["id1"], "table": "persons"], index: 0), + .REDACTION_KEY_ERROR(value: "0")) + } + + func testCheckGetByIdEntryInvalidRedactionType() { + var entry = validGetByIdEntry + entry["redaction"] = "PLAIN_TEXT" + + assertError(RequestValidators.checkGetByIdEntry(entry: entry, index: 0), .INVALID_REDACTION_TYPE()) + } + + // MARK: - validateGetRecords (branches not covered by skyflow-iOS-getTests) + + func testValidateGetRecordsEmptyTableName() { + let entry: [String: Any] = ["ids": ["id1"], "table": "", "redaction": RedactionType.PLAIN_TEXT] + + assertError(RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(), index: 0), + .EMPTY_TABLE_NAME()) + } + + func testValidateGetRecordsColumnNameWithoutColumnValues() { + let entry: [String: Any] = ["table": "persons", "columnName": "email", "redaction": RedactionType.PLAIN_TEXT] + + assertError(RequestValidators.validateGetRecords(entry: entry, getOptions: GetOptions(), index: 0), + .MISSING_RECORD_COLUMN_VALUE()) + } +} diff --git a/Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift b/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift similarity index 100% rename from Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift rename to Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift diff --git a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/ClientScenario.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/ClientScenario.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/ClientScenario.swift diff --git a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/DemoImpl.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/DetokenizeScenario.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/DetokenizeScenario.swift diff --git a/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/GetByIdScenario.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/GetByIdScenario.swift diff --git a/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/InsertScenario.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/InsertScenario.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/InsertScenario.swift diff --git a/Tests/skyflow-iOS-scenarioTests/TestData.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/TestData.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift diff --git a/Tests/skyflow-iOS-scenarioTests/XCTestManifests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/XCTestManifests.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/XCTestManifests.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/XCTestManifests.swift diff --git a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift diff --git a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift diff --git a/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift similarity index 100% rename from Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift rename to Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift diff --git a/Tests/skyflow-iOS-collectTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-collectTests/DemoImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-collectTests/XCTestManifests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/XCTestManifests.swift similarity index 100% rename from Tests/skyflow-iOS-collectTests/XCTestManifests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-collectTests/XCTestManifests.swift diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift similarity index 100% rename from Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift similarity index 100% rename from Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift diff --git a/Tests/skyflow-iOS-composableTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-composableTests/DemoImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift similarity index 96% rename from Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index e000e6ae..149cb91c 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -66,6 +66,26 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { } XCTAssertEqual(bstyle.borderColor, UIColor.blue) } + func testContainerOptionsEmptyLayoutArray() { + let container = skyflow.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [])) + + let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) + + let styles = Styles(base: bstyle) + + let options = CollectElementOptions(required: false) + + let collectInput = CollectElementInput(tableName: "persons", column: "cardnumber", inputStyles: styles, placeholder: "card number", type: .CARD_NUMBER) + + _ = container?.create(input: collectInput, options: options) + + do { + _ = try container?.getComposableView() + } catch { + XCTAssertEqual(error.localizedDescription.description, SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "\(ErrorCodes.EMPTY_COMPOSABLE_LAYOUT_ARRAY().description)" ]).localizedDescription) + } + XCTAssertEqual(bstyle.borderColor, UIColor.blue) + } func testContainerOptionsLayoutIsless() { let container = skyflow.container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [0])) diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift new file mode 100644 index 00000000..4085a8fd --- /dev/null +++ b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Branch coverage for the FlowVault composable container's collect() flow: +// element-state errors, typed additionalFields/upsert validation and the full +// request-building path (driven against an unresolvable vault host). + +import UIKit +import XCTest +@testable import SkyflowFlowVault +@testable import SkyflowCore + +final class skyflow_iOS_composableFlowTests: XCTestCase { + + // Token provider that succeeds synchronously, so flow tests never depend + // on the TOKEN_ENDPOINT environment the shared DemoTokenProvider uses. + private class InstantTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + apiCallback.onSuccess("token") + } + } + + private func offlineClient() -> Client { + return Client(Configuration(vaultID: "vault_id", vaultURL: "https://testvault.skyflow.invalid/", + tokenProvider: InstantTokenProvider())) + } + + func testComposableCollectUnmountedElementFails() { + let container = offlineClient().container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + _ = container!.create(input: CollectElementInput(tableName: "persons", column: "name", type: .CARDHOLDER_NAME)) + + let expectation = XCTestExpectation(description: "composable collect with unmounted element should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNMOUNTED_COLLECT_ELEMENT(value: "name").description) + } + + func testComposableCollectEmptyAdditionalFieldsRecordsFails() { + let container = offlineClient().container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + let element = container!.create(input: CollectElementInput(tableName: "persons", column: "name", type: .CARDHOLDER_NAME)) + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "composable collect with empty additionalFields should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: CollectOptions(additionalFields: AdditionalFields(records: []))) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_RECORDS_OBJECT().description) + } + + func testComposableCollectEmptyUpsertFails() { + let container = offlineClient().container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + let element = container!.create(input: CollectElementInput(tableName: "persons", column: "name", type: .CARDHOLDER_NAME)) + UIWindow().addSubview(element) + + let expectation = XCTestExpectation(description: "composable collect with empty upsert should fail") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: CollectOptions(upsert: [])) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().description) + } + + func testComposableCollectValidElementRunsFullRequestFlow() { + let container = offlineClient().container(type: ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1])) + let element = container!.create(input: CollectElementInput(tableName: "persons", column: "name", type: .CARDHOLDER_NAME)) + element.textField.secureText = "John" + UIWindow().addSubview(element) + + let options = CollectOptions( + additionalFields: AdditionalFields(records: [AdditionalFieldsRecord(tableName: "contacts", data: ["email": "a@b.com"])]), + upsert: [UpsertOption(tableName: "persons", uniqueColumns: ["name"])]) + + let expectation = XCTestExpectation(description: "composable collect should reach the network layer and fail offline") + let callback = DemoAPICallback(expectation: expectation) + container!.collect(callback: callback, options: options) + + wait(for: [expectation], timeout: 20.0) + // The whole request-building path (validators, CollectRequestBuilder, CVV + // capture, postAndUpdate) executed; the unresolvable host then failed it. + XCTAssertFalse(callback.receivedResponse == "default" && callback.data.isEmpty) + } +} diff --git a/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/CollectElementOptionsTests.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/CollectElementOptionsTests.swift diff --git a/Tests/skyflow-iOS-elementTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/DemoImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/InputFormattingTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift diff --git a/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift diff --git a/Tests/skyflow-iOS-elementTests/XCTestManifests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/XCTestManifests.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/XCTestManifests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/XCTestManifests.swift diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift similarity index 100% rename from Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift diff --git a/Tests/skyflow-iOS-errorTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/DemoImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-errorTests/GatewayImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/GatewayImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift diff --git a/Tests/skyflow-iOS-errorTests/XCTestManifests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/XCTestManifests.swift similarity index 100% rename from Tests/skyflow-iOS-errorTests/XCTestManifests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-errorTests/XCTestManifests.swift diff --git a/Tests/skyflow-iOS-revealTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-revealTests/DemoImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealElementOptionsTests.swift similarity index 100% rename from Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealElementOptionsTests.swift diff --git a/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift similarity index 100% rename from Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift diff --git a/Tests/skyflow-iOS-revealTests/XCTestManifests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/XCTestManifests.swift similarity index 100% rename from Tests/skyflow-iOS-revealTests/XCTestManifests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-revealTests/XCTestManifests.swift diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift similarity index 100% rename from Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift diff --git a/Tests/skyflow-iOS-utilTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/DemoImpl.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/DemoImpl.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/DemoImpl.swift diff --git a/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/MockURLProtocol.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/MockURLProtocol.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/MockURLProtocol.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenLifecycleTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenLifecycleTests.swift new file mode 100644 index 00000000..e03174d0 --- /dev/null +++ b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenLifecycleTests.swift @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Tests for the shared bearer-token lifecycle in SkyflowCore: APIClient.isTokenValid(), +// APIClient.getAccessToken() and TokenAPICallback. This layer is contract-agnostic and +// used identically by both SDKs. + +import XCTest +@testable import SkyflowFlowVault +@testable import SkyflowCore + +final class TokenLifecycleTests: XCTestCase { + + // A TokenProvider that fails the test if it's ever invoked, for asserting that a + // still-valid cached token is reused without refreshing. + private final class FailIfCalledTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + XCTFail("token provider should not be called while the cached token is still valid") + } + } + + private final class RecordingCallback: Callback { + var successValue: Any? + var failureValue: Any? + + func onSuccess(_ responseBody: Any) { + successValue = responseBody + } + + func onFailure(_ error: Any) { + failureValue = error + } + } + + // Builds a JWT-shaped "header.payload" token whose payload is valid, decodable JSON + // carrying the given "exp" claim -- the only shape isTokenValid() can evaluate without + // hitting one of its internal force-casts. + private func jwt(exp: Int) -> String { + let payload = try! JSONSerialization.data(withJSONObject: ["exp": exp]) + return "header.\(payload.base64EncodedString())" + } + + // MARK: - APIClient.isTokenValid() + + func testIsTokenValidFalseForEmptyToken() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + XCTAssertFalse(apiClient.isTokenValid()) + } + + func testIsTokenValidFalseForTokenWithoutPayloadSeparator() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + apiClient.token = "no-dot-separator" + XCTAssertFalse(apiClient.isTokenValid()) + } + + func testIsTokenValidFalseForNonJSONPayload() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + let payload = Data("not json {{{".utf8).base64EncodedString() + apiClient.token = "header.\(payload)" + XCTAssertFalse(apiClient.isTokenValid()) + } + + func testIsTokenValidFalseForExpiredToken() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + apiClient.token = jwt(exp: 1) // 1970-01-01T00:00:01Z, long expired + XCTAssertFalse(apiClient.isTokenValid()) + } + + func testIsTokenValidTrueForFutureExpiryToken() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + apiClient.token = jwt(exp: 9_999_999_999) // year 2286 + XCTAssertTrue(apiClient.isTokenValid()) + } + + // MARK: - APIClient.getAccessToken() + + func testGetAccessTokenReusesStillValidTokenWithoutCallingProvider() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: FailIfCalledTokenProvider()) + let validToken = jwt(exp: 9_999_999_999) + apiClient.token = validToken + + let callback = RecordingCallback() + apiClient.getAccessToken(callback: callback, contextOptions: ContextOptions()) + + XCTAssertEqual(callback.successValue as? String, validToken) + XCTAssertNil(callback.failureValue) + } + + func testGetAccessTokenRefreshesWhenTokenMissing() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + XCTAssertEqual(apiClient.token, "") + + let callback = RecordingCallback() + apiClient.getAccessToken(callback: callback, contextOptions: ContextOptions()) + + // DemoTokenProvider hands back the raw string "dummy_token", which isn't a + // well-formed bearer token, so TokenAPICallback should reject it. + XCTAssertNil(callback.successValue) + XCTAssertNotNil(callback.failureValue) + XCTAssertEqual(apiClient.token, "") // reverted, not left as the malformed value + } + + // MARK: - TokenAPICallback + + func testTokenAPICallbackAcceptsWellFormedToken() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + let callback = RecordingCallback() + let tokenApiCallback = TokenAPICallback(callback: callback, apiClient: apiClient, contextOptions: ContextOptions()) + + let newToken = jwt(exp: 9_999_999_999) + tokenApiCallback.onSuccess(newToken) + + XCTAssertEqual(callback.successValue as? String, newToken) + XCTAssertNil(callback.failureValue) + XCTAssertEqual(apiClient.token, newToken) + } + + func testTokenAPICallbackRevertsAndFailsOnMalformedStringToken() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + apiClient.token = jwt(exp: 9_999_999_999) + let previousToken = apiClient.token + + let callback = RecordingCallback() + let tokenApiCallback = TokenAPICallback(callback: callback, apiClient: apiClient, contextOptions: ContextOptions()) + + tokenApiCallback.onSuccess("not-a-jwt") + + XCTAssertNil(callback.successValue) + let error = (callback.failureValue as? NSError) + XCTAssertEqual(error?.localizedDescription, ErrorCodes.INVALID_BEARER_TOKEN_FORMAT().description) + XCTAssertEqual(apiClient.token, previousToken) // reverted rather than left malformed + } + + func testTokenAPICallbackFailsOnNonStringResponseBody() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + let callback = RecordingCallback() + let tokenApiCallback = TokenAPICallback(callback: callback, apiClient: apiClient, contextOptions: ContextOptions()) + + tokenApiCallback.onSuccess(["unexpected": "shape"]) + + XCTAssertNil(callback.successValue) + let error = (callback.failureValue as? NSError) + XCTAssertEqual(error?.localizedDescription, ErrorCodes.INVALID_BEARER_TOKEN_FORMAT().description) + } + + func testTokenAPICallbackPassesThroughProviderFailure() { + let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) + let callback = RecordingCallback() + let tokenApiCallback = TokenAPICallback(callback: callback, apiClient: apiClient, contextOptions: ContextOptions()) + + let providerError = NSError(domain: "TokenProviderDomain", code: 42, userInfo: [NSLocalizedDescriptionKey: "provider blew up"]) + tokenApiCallback.onFailure(providerError) + + XCTAssertNil(callback.successValue) + XCTAssertTrue((callback.failureValue as? NSError) === providerError) + } +} diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenParsingTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenParsingTests.swift new file mode 100644 index 00000000..7f921096 --- /dev/null +++ b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenParsingTests.swift @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Tests for the typed Token model and CollectRecord.tokens parsing +// ([String: [Token]]?, keyed by column name). + +import XCTest +@testable import SkyflowFlowVault +@testable import SkyflowCore + +final class TokenParsingTests: XCTestCase { + + private func collectRecord(fields: Any?) -> CollectRecord { + var dict: [String: Any] = ["tableName": "persons", "skyflowID": "id1", "httpCode": 200] + if let fields = fields { + dict["fields"] = fields + } + return CollectResponse(["records": [dict]])!.records[0] + } + + // MARK: - Token + + func testTokenPublicInit() { + let token = Token(token: "tok-1", tokenGroupName: "deterministic") + + XCTAssertEqual(token.token, "tok-1") + XCTAssertEqual(token.tokenGroupName, "deterministic") + } + + func testTokenPublicInitDefaultsTokenGroupNameToNil() { + XCTAssertNil(Token(token: "tok-1").tokenGroupName) + } + + // MARK: - CollectRecord.tokens parsing + + func testTokensNilWhenFieldsKeyAbsent() { + XCTAssertNil(collectRecord(fields: nil).tokens) + } + + func testTokensParsesSingleTokenPerColumn() throws { + let record = collectRecord(fields: [ + "card_number": [["token": "tok-1", "tokenGroupName": "vault"]] + ]) + + let tokens = try XCTUnwrap(record.tokens) + XCTAssertEqual(tokens["card_number"]?.count, 1) + XCTAssertEqual(tokens["card_number"]?.first?.token, "tok-1") + XCTAssertEqual(tokens["card_number"]?.first?.tokenGroupName, "vault") + } + + func testTokensParsesMultipleTokenGroupsForSameColumn() throws { + let record = collectRecord(fields: [ + "card_number": [ + ["token": "tok-deterministic", "tokenGroupName": "deterministic"], + ["token": "tok-vault", "tokenGroupName": "vault"] + ] + ]) + + let tokens = try XCTUnwrap(record.tokens?["card_number"]) + XCTAssertEqual(tokens.count, 2) + XCTAssertEqual(Set(tokens.map { $0.token }), ["tok-deterministic", "tok-vault"]) + } + + func testTokensParsesMultipleColumns() { + let record = collectRecord(fields: [ + "card_number": [["token": "tok-1", "tokenGroupName": "vault"]], + "cvv": [["token": "tok-2", "tokenGroupName": "vault"]] + ]) + + XCTAssertEqual(record.tokens?["card_number"]?.first?.token, "tok-1") + XCTAssertEqual(record.tokens?["cvv"]?.first?.token, "tok-2") + } + + func testTokensOmitsTokenGroupNameWhenAbsent() { + let record = collectRecord(fields: [ + "card_number": [["token": "tok-1"]] + ]) + + XCTAssertEqual(record.tokens?["card_number"]?.first?.token, "tok-1") + XCTAssertNil(record.tokens?["card_number"]?.first?.tokenGroupName) + } + + func testTokensDropsEntryMissingTokenKey() throws { + let record = collectRecord(fields: [ + "card_number": [ + ["tokenGroupName": "vault"], + ["token": "tok-1", "tokenGroupName": "deterministic"] + ] + ]) + + let tokens = try XCTUnwrap(record.tokens?["card_number"]) + XCTAssertEqual(tokens.count, 1) + XCTAssertEqual(tokens.first?.token, "tok-1") + } + + func testTokensDropsEntryWithNonStringTokenValue() { + let record = collectRecord(fields: [ + "card_number": [["token": 12345]] + ]) + + XCTAssertEqual(record.tokens?["card_number"]?.count, 0) + } + + func testTokensDropsColumnWithWrongShape() { + // "fields" is expected to be a dict of column -> [token dict]; a column whose + // value isn't an array of dictionaries at all is dropped rather than crashing. + let record = collectRecord(fields: [ + "card_number": "not an array" + ]) + + XCTAssertNil(record.tokens?["card_number"]) + } + + func testTokensNilWhenFieldsIsJSONNull() { + // JSONSerialization represents a JSON `null` as NSNull, not Swift nil/absence. + let record = collectRecord(fields: NSNull()) + + XCTAssertNil(record.tokens) + } + + func testTokensDropsColumnWhenArrayContainsNonDictionaryEntry() { + // A conditional cast to [[String: Any]] fails for the whole array if even one + // element isn't a dictionary - the column is dropped, not partially parsed. + let mixedArray: [Any] = [["token": "tok-1"], "not a dict"] + let record = collectRecord(fields: ["card_number": mixedArray]) + + XCTAssertNil(record.tokens?["card_number"]) + } + + // MARK: - Whole record / whole response missing or malformed + + func testCollectRecordFromEmptyDictionaryHasAllNilFields() { + let record = CollectResponse(["records": [[:] as [String: Any]]])!.records[0] + + XCTAssertNil(record.tableName) + XCTAssertNil(record.skyflowId) + XCTAssertNil(record.tokens) + XCTAssertNil(record.hashedData) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + } + + func testCollectResponseNilWhenRecordsKeyAbsent() { + XCTAssertNil(CollectResponse(["norecords": []])) + } + + func testCollectResponseNilWhenTopLevelIsNotADictionary() { + XCTAssertNil(CollectResponse("not a dictionary")) + XCTAssertNil(CollectResponse(["just", "an", "array"])) + XCTAssertNil(CollectResponse(NSNull())) + } + + func testCollectResponseNilWhenRecordsContainsNonDictionaryEntry() { + // Same whole-array-cast-fails behavior as the per-column tokens array: one + // non-dictionary entry in "records" fails the entire response, not just that entry. + let mixedRecords: [Any] = [["tableName": "persons"], "not a dict"] + XCTAssertNil(CollectResponse(["records": mixedRecords])) + } + + func testCollectResponseEmptyRecordsArray() { + XCTAssertEqual(CollectResponse(["records": [] as [[String: Any]]])?.records.count, 0) + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift new file mode 100644 index 00000000..97dae990 --- /dev/null +++ b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Tests for FlowVault (v2) SDK-specific surface not covered elsewhere: +// initialize() identity, the public CollectCallback/RevealCallback wrappers +// and the typed option/input models. + +import XCTest +@testable import SkyflowFlowVault +@testable import SkyflowCore + +final class skyflow_iOS_flowVaultSdkTests: XCTestCase { + + // MARK: - initialize() + + func testInitializeStampsFlowVaultSdkName() { + // SDK_NAME is a process-wide global shared with the legacy SDK's + // tests (and asserted against its default by testGetDeviceDetails); + // restore it so this test can't leak into other suites. + let originalSdkName = SDK_NAME + defer { SDK_NAME = originalSdkName } + + let client = SkyflowFlowVault.initialize(Configuration( + vaultID: "vault_id", + vaultURL: "https://example.org/", + tokenProvider: DemoTokenProvider())) + + XCTAssertNotNil(client) + XCTAssertEqual(SDK_NAME, "skyflow-flowvault-ios") + } + + // MARK: - CollectCallback wrapper + + func testCollectCallbackUnwrapsValidResponse() { + let expectation = XCTestExpectation(description: "success handler should receive CollectResponse") + let callback = CollectCallback( + onSuccess: { response in + XCTAssertEqual(response.records.count, 2) + XCTAssertNil(response.records[0].error) + XCTAssertEqual(response.records[1].error, "failed") + expectation.fulfill() + }, + onFailure: { _ in XCTFail("valid body must not route to onFailure") }) + + callback.onSuccess([ + "records": [ + ["tableName": "persons", "skyflowID": "id1", "httpCode": 200], + ["error": "failed", "httpCode": 400] + ] + ] as [String: Any]) + + wait(for: [expectation], timeout: 10.0) + } + + func testCollectCallbackRoutesMalformedBodyToFailure() { + let expectation = XCTestExpectation(description: "malformed body should route to onFailure") + let callback = CollectCallback( + onSuccess: { _ in XCTFail("malformed body must not route to onSuccess") }, + onFailure: { _ in expectation.fulfill() }) + + callback.onSuccess("not a dictionary") + + wait(for: [expectation], timeout: 10.0) + } + + func testCollectCallbackWrapsFailureAsSkyflowError() { + let expectation = XCTestExpectation(description: "failure should be a normalized SkyflowError") + let callback = CollectCallback( + onSuccess: { _ in XCTFail("failure must not route to onSuccess") }, + onFailure: { error in + XCTAssertEqual(error.message, "The Internet connection appears to be offline.") + expectation.fulfill() + }) + + callback.onFailure(NSError(domain: "NSURLErrorDomain", code: -1009, + userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."])) + + wait(for: [expectation], timeout: 10.0) + } + + // MARK: - RevealCallback wrapper + + func testRevealCallbackUnwrapsValidResponse() { + let expectation = XCTestExpectation(description: "success handler should receive RevealResponse") + let callback = RevealCallback( + onSuccess: { response in + XCTAssertEqual(response.records.count, 1) + XCTAssertEqual(response.records[0].token, "token1") + expectation.fulfill() + }, + onFailure: { _ in XCTFail("valid body must not route to onFailure") }) + + callback.onSuccess(["records": [["token": "token1", "value": "John", "httpCode": 200]]] as [String: Any]) + + wait(for: [expectation], timeout: 10.0) + } + + func testRevealCallbackRoutesMalformedBodyToFailure() { + let expectation = XCTestExpectation(description: "malformed body should route to onFailure") + let callback = RevealCallback( + onSuccess: { _ in XCTFail("malformed body must not route to onSuccess") }, + onFailure: { _ in expectation.fulfill() }) + + callback.onSuccess(["records": "not an array"]) + + wait(for: [expectation], timeout: 10.0) + } + + func testRevealCallbackWrapsFailureAsSkyflowError() { + let expectation = XCTestExpectation(description: "failure should be a normalized SkyflowError") + let callback = RevealCallback( + onSuccess: { _ in XCTFail("failure must not route to onSuccess") }, + onFailure: { error in + XCTAssertEqual(error.message, "TokenProvider error") + expectation.fulfill() + }) + + callback.onFailure(NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "TokenProvider error"])) + + wait(for: [expectation], timeout: 10.0) + } + + // MARK: - Typed models + + func testUpsertOptionCarriesTypedFields() { + let option = UpsertOption(tableName: "persons", uniqueColumns: ["email"], updateType: .REPLACE) + + XCTAssertEqual(option.tableName, "persons") + XCTAssertEqual(option.uniqueColumns, ["email"]) + XCTAssertEqual(option.updateType, .REPLACE) + XCTAssertNil(UpsertOption(tableName: "persons", uniqueColumns: ["email"]).updateType) + } + + func testAdditionalFieldsRecordDefaultsToNilSkyflowId() { + let record = AdditionalFieldsRecord(tableName: "persons", data: ["name": "john"]) + + XCTAssertEqual(record.tableName, "persons") + XCTAssertNil(record.skyflowId) + + let update = AdditionalFieldsRecord(tableName: "persons", data: ["name": "john"], skyflowId: "sid-1") + XCTAssertEqual(update.skyflowId, "sid-1") + } + + func testTokenGroupRedactionCarriesFields() { + let redaction = TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + + XCTAssertEqual(redaction.tokenGroupName, "deterministic_string") + XCTAssertEqual(redaction.redaction, "MASKED") + } + + // MARK: - RequestValidators valid (nil-returning) paths + + func testCheckRecordValidReturnsNil() { + XCTAssertNil(RequestValidators.checkRecord( + record: AdditionalFieldsRecord(tableName: "persons", data: ["name": "john"]), index: 0)) + } + + func testCheckAdditionalFieldsValidReturnsNil() { + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(tableName: "persons", data: ["name": "john"]), + AdditionalFieldsRecord(tableName: "contacts", data: ["email": "a@b.com"], skyflowId: "sid-1") + ]) + + XCTAssertNil(RequestValidators.checkAdditionalFields(additionalFields)) + } + + func testCheckTokenGroupRedactionsValidReturnsNil() { + XCTAssertNil(RequestValidators.checkTokenGroupRedactions([ + TokenGroupRedaction(tokenGroupName: "group", redaction: "MASKED") + ])) + } + + func testCheckUpsertOptionsValidReturnsNil() { + XCTAssertNil(RequestValidators.checkUpsertOptions([ + UpsertOption(tableName: "persons", uniqueColumns: ["email"]) + ])) + } + + // MARK: - Deprecated CollectElementInput initializer (backward compatibility) + + @available(*, deprecated) // silences the warning for exercising the deprecated altText: initializer + func testCollectElementInputDeprecatedAltTextInit() { + let input = CollectElementInput(tableName: "persons", column: "name", altText: "alt", type: .CARDHOLDER_NAME) + + XCTAssertEqual(input.data.tableName, "persons") + XCTAssertEqual(input.data.column, "name") + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift similarity index 100% rename from Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift rename to Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift From b5e8228895cea6616a84c997c1409938740f4402 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 14 Aug 2026 18:56:24 +0530 Subject: [PATCH 07/16] SK-3043 add type for tokens --- .../models/RevealMetadata.swift | 23 ++++++++ .../models/RevealResponse.swift | 17 +----- .../skyflow_iOS_revealUtilTests.swift | 55 +++++++++++++++++-- 3 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 Sources/SkyflowFlowVault/models/RevealMetadata.swift diff --git a/Sources/SkyflowFlowVault/models/RevealMetadata.swift b/Sources/SkyflowFlowVault/models/RevealMetadata.swift new file mode 100644 index 00000000..4e9a8b6c --- /dev/null +++ b/Sources/SkyflowFlowVault/models/RevealMetadata.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Typed shape for RevealRecord.metadata, e.g. {"tableName": "customer", "skyflowId": "..."} - +// the exact shape reported from a real device run. + +import Foundation + +public struct RevealMetadata { + public let tableName: String? + public let skyflowId: String? + + public init(tableName: String? = nil, skyflowId: String? = nil) { + self.tableName = tableName + self.skyflowId = skyflowId + } + init?(dict: [String: Any]?) { + guard let dict = dict else { return nil } + self.tableName = dict["tableName"] as? String + self.skyflowId = (dict["skyflowID"] as? String) ?? (dict["skyflowId"] as? String) + } +} diff --git a/Sources/SkyflowFlowVault/models/RevealResponse.swift b/Sources/SkyflowFlowVault/models/RevealResponse.swift index 9730f432..e47d5536 100644 --- a/Sources/SkyflowFlowVault/models/RevealResponse.swift +++ b/Sources/SkyflowFlowVault/models/RevealResponse.swift @@ -31,28 +31,15 @@ public struct RevealResponse { public struct RevealRecord { public let token: String? public let tokenGroupName: String? - // Keyed by whatever fields the vault includes (e.g. "skyflowId", "tableName"). - public let metadata: [String: Any]? + public let metadata: RevealMetadata? public let httpCode: Int public let error: String? init(_ dict: [String: Any]) { self.token = dict["token"] as? String self.tokenGroupName = dict["tokenGroupName"] as? String - self.metadata = RevealRecord.normalizeMetadataKeys(dict["metadata"] as? [String: Any]) + self.metadata = RevealMetadata(dict: dict["metadata"] as? [String: Any]) self.httpCode = dict["httpCode"] as? Int ?? 0 self.error = dict["error"] as? String } - - // The vault sends this key as "skyflowID" - normalized here to "skyflowId" so metadata's - // casing is consistent with Skyflow.CollectRecord.skyflowId rather than exposing the raw - // wire casing directly. - private static func normalizeMetadataKeys(_ metadata: [String: Any]?) -> [String: Any]? { - guard let metadata = metadata else { return nil } - var normalized: [String: Any] = [:] - for (key, value) in metadata { - normalized[key == "skyflowID" ? "skyflowId" : key] = value - } - return normalized - } } diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index 0be3316b..7317b5bf 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -703,9 +703,8 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { func testRevealRecordNormalizesSkyflowIDKeyInMetadata() { let record = RevealRecord(["token": "abc", "httpCode": 200, "metadata": ["skyflowID": "SID", "tableName": "table"]]) let metadata = record.metadata! - XCTAssertEqual(metadata["skyflowId"] as? String, "SID") - XCTAssertNil(metadata["skyflowID"]) - XCTAssertEqual(metadata["tableName"] as? String, "table") + XCTAssertEqual(metadata.skyflowId, "SID") + XCTAssertEqual(metadata.tableName, "table") } // Exact metadata shape reported from a real device run: {"skyflowID": "426279b9-...", @@ -718,9 +717,53 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { ]) print("metadata:", record.metadata as Any) let metadata = record.metadata! - XCTAssertEqual(metadata["skyflowId"] as? String, "426279b9-efe0-44df-bc39-16839bc86e86") - XCTAssertNil(metadata["skyflowID"]) - XCTAssertEqual(metadata["tableName"] as? String, "table5") + XCTAssertEqual(metadata.skyflowId, "426279b9-efe0-44df-bc39-16839bc86e86") + XCTAssertEqual(metadata.tableName, "table5") + } + + func testRevealRecordMetadataAcceptsAlreadyNormalizedSkyflowIdKey() { + // Not currently the wire's own casing, but accepted so consumers constructing a + // RevealRecord directly (e.g. in tests) don't need to know the wire spelling. + let record = RevealRecord(["token": "abc", "httpCode": 200, "metadata": ["skyflowId": "SID", "tableName": "table"]]) + + XCTAssertEqual(record.metadata?.skyflowId, "SID") + XCTAssertEqual(record.metadata?.tableName, "table") + } + + func testRevealRecordMetadataNilWhenKeyAbsent() { + let record = RevealRecord(["token": "abc", "httpCode": 200]) + + XCTAssertNil(record.metadata) + } + + func testRevealRecordMetadataNilWhenJSONNull() { + let record = RevealRecord(["token": "abc", "httpCode": 200, "metadata": NSNull()]) + + XCTAssertNil(record.metadata) + } + + func testRevealRecordMetadataPresentWithNilFieldsWhenKeysMissing() { + // The dict itself is valid, just missing both known keys - metadata is a present, + // non-nil RevealMetadata whose individual fields are nil, not a dropped/nil metadata. + let record = RevealRecord(["token": "abc", "httpCode": 200, "metadata": [:] as [String: Any]]) + + XCTAssertNotNil(record.metadata) + XCTAssertNil(record.metadata?.tableName) + XCTAssertNil(record.metadata?.skyflowId) + } + + func testRevealMetadataPublicInit() { + let metadata = RevealMetadata(tableName: "persons", skyflowId: "sid-1") + + XCTAssertEqual(metadata.tableName, "persons") + XCTAssertEqual(metadata.skyflowId, "sid-1") + } + + func testRevealMetadataPublicInitDefaultsToNilFields() { + let metadata = RevealMetadata() + + XCTAssertNil(metadata.tableName) + XCTAssertNil(metadata.skyflowId) } func testRevealResponseInitReturnsNilForMalformedBody() { From 300139a2abc0285bd04d9becfd855bd2edfddf58 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 14 Aug 2026 19:51:49 +0530 Subject: [PATCH 08/16] SK-3043 fix tests --- .../skyflow-iOS-getByIdTests/DemoImpl.swift | 2 +- .../skyflow_iOS_getByIdTests.swift | 14 ++++---- .../skyflow-iOS-getTests/DemoImpl.swift | 2 +- .../skyflow_iOS_getTests.swift | 4 +-- .../skyflow_iOS_validateGetRecordTest.swift | 4 +-- .../skyflow-iOS-scenarioTests/DemoImpl.swift | 2 +- .../skyflow-iOS-scenarioTests/TestData.swift | 36 +++++++++++++++++++ .../skyflow_iOS_detokenizeScenarioTests.swift | 2 +- .../skyflow_iOS_getByIdScenarioTests.swift | 2 +- .../skyflow_iOS_insertScenarioTests.swift | 2 +- .../skyflow-iOS-collectTests/DemoImpl.swift | 2 +- .../skyflow_iOS_collectTests.swift | 8 ++--- .../DemoImpl.swift | 2 +- ...skyflow_iOS_composableEelementsTests.swift | 4 +-- .../skyflow-iOS-elementTests/DemoImpl.swift | 2 +- .../InputFormattingTests.swift | 4 +-- .../TextFieldDelegateTests.swift | 4 +-- .../skyflow-iOS-errorTests/DemoImpl.swift | 2 +- .../skyflow-iOS-errorTests/GatewayImpl.swift | 2 +- .../Skyflow_iOS_revealErrorTests.swift | 4 +-- .../skyflow-iOS-revealTests/DemoImpl.swift | 2 +- .../skyflow_iOS_revealTests.swift | 10 +++--- 22 files changed, 76 insertions(+), 40 deletions(-) diff --git a/Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift index 698dc696..5dbbf7b9 100644 --- a/Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift +++ b/Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift b/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift index 276ec1a9..419e442e 100644 --- a/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift +++ b/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift @@ -20,8 +20,8 @@ class skyflow_iOS_getByIdTests: XCTestCase { override func setUp() { self.skyflow = Client(Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) } @@ -40,21 +40,21 @@ class skyflow_iOS_getByIdTests: XCTestCase { } let invalidTokenProvider = InvalidTokenProvider() - let skyflow = Client(Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, tokenProvider: invalidTokenProvider)) + let skyflow = Client(Configuration(vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: invalidTokenProvider)) let records = [ "records": [ [ "ids": [ - ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID1"]!, - ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID2"]!, - ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID3"]! + (ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID1"] ?? "dummy_skyflow_id_1"), + (ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID2"] ?? "dummy_skyflow_id_2"), + (ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID3"] ?? "dummy_skyflow_id_3") ], "table": "persons", "redaction": RedactionType.PLAIN_TEXT ], [ "ids": [ - ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID3"]! + (ProcessInfo.processInfo.environment["TEST_SKYFLOW_ID3"] ?? "dummy_skyflow_id_3") ], "table": "persons", "redaction": RedactionType.PLAIN_TEXT diff --git a/Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift index 997f488f..aaaed85d 100644 --- a/Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift +++ b/Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift index 53cf5ccd..1cd79776 100644 --- a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift +++ b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift @@ -15,8 +15,8 @@ class skyflow_iOS_getTests: XCTestCase { override func setUp() { self.skyflow = Client(Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) } diff --git a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift index 48fb4bf6..da684ded 100644 --- a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift +++ b/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift @@ -15,8 +15,8 @@ class skyflow_iOS_ValidateGetRecordTest: XCTestCase { override func setUp() { self.skyflow = Client(Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) } diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift index c8509fc9..bdd8063f 100644 --- a/Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift +++ b/Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift index 4e790ce8..3b6befb1 100644 --- a/Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift +++ b/Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift @@ -9,6 +9,42 @@ struct TestData : Codable { var CLIENT: ClientData var VAULT: VaultData var CONNECTION: ConnectionData + + // Used when the TEST_DATA env var (normally supplied via CI secrets) isn't set, so running + // these scenario tests locally fails at the network layer instead of crashing at setUp. + static let dummyJSON = """ + { + "CLIENT": { + "INVALID_VAULT_ID": "invalid_vault_id", + "INVALID_VAULT_URL": "https://invalid.vault.skyflowapis.dev/", + "VAULT_ID": "dummy_vault_id", + "VAULT_URL": "https://dummy.vault.skyflowapis.dev/" + }, + "VAULT": { + "TABLE_NAME": "persons", + "INVALID_TABLE_NAME": "invalid_table", + "VALID_FIELDS": [{"NAME": "name", "VALUE": "john"}], + "INVALID_FIELD": {"NAME": "invalid_field", "VALUE": "value"}, + "VALID_TOKENS": ["dummy_token_1"], + "INVALID_TOKEN": "invalid_token", + "VALID_IDS": ["dummy_id_1"], + "INVALID_ID": "invalid_id" + }, + "CONNECTION": { + "INVALID_URL": "https://invalid.connection.dummy/", + "VALID_URL": "https://dummy.connection.dummy/", + "PARAMS": { + "PATH_PARAM": {"NAME": "path", "VALUE": "value"}, + "QUERY_PARAM": {"NAME": "query", "VALUE": "value"}, + "INVALID_PARAM": {"NAME": "invalid", "VALUE": "value"} + }, + "REQUEST_BODY": {"JSON": {}, "XML": ""}, + "INVALID_REQUEST_BODY": {"JSON": {}, "XML": ""}, + "RESPONSE_BODY": {"JSON": {}, "XML": ""}, + "INVALID_RESPONSE_BODY": {"JSON": {}, "XML": ""} + } + } + """ } struct ClientData: Codable { diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift index e2277ac5..5d644e2e 100644 --- a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift +++ b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift @@ -12,7 +12,7 @@ final class skyflow_iOS_detokenizeScenarioTests: XCTestCase { var skyflow: Client! var tokenProvider: TokenProvider! - let testData = try! JSONDecoder().decode(TestData.self, from: Data(ProcessInfo.processInfo.environment["TEST_DATA"]!.utf8)) + let testData = try! JSONDecoder().decode(TestData.self, from: Data((ProcessInfo.processInfo.environment["TEST_DATA"] ?? TestData.dummyJSON).utf8)) override func setUp() { self.skyflow = Client(Configuration(tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift index 856f7431..eb0530ce 100644 --- a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift +++ b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift @@ -12,7 +12,7 @@ final class skyflow_iOS_getByIdScenarioTests: XCTestCase { var skyflow: Client! var tokenProvider: TokenProvider! - let testData = try! JSONDecoder().decode(TestData.self, from: Data(ProcessInfo.processInfo.environment["TEST_DATA"]!.utf8)) + let testData = try! JSONDecoder().decode(TestData.self, from: Data((ProcessInfo.processInfo.environment["TEST_DATA"] ?? TestData.dummyJSON).utf8)) override func setUp() { self.skyflow = Client(Configuration(tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift index fe95350d..44aab239 100644 --- a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift +++ b/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift @@ -12,7 +12,7 @@ final class skyflow_iOS_insertScenarioTests: XCTestCase { var skyflow: Client! var tokenProvider: TokenProvider! - let testData = try! JSONDecoder().decode(TestData.self, from: Data(ProcessInfo.processInfo.environment["TEST_DATA"]!.utf8)) + let testData = try! JSONDecoder().decode(TestData.self, from: Data((ProcessInfo.processInfo.environment["TEST_DATA"] ?? TestData.dummyJSON).utf8)) override func setUp() { self.skyflow = Client(Configuration(tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift index 28e6485d..bb747e75 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift @@ -16,7 +16,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]! ) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token") ) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index 6647a325..e3c8c632 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -13,8 +13,8 @@ final class skyflow_iOS_collectTests: XCTestCase { override func setUp() { self.skyflow = SkyflowFlowVault.initialize( Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG, env: .DEV)) ) @@ -362,8 +362,8 @@ final class skyflow_iOS_collectTests: XCTestCase { let skyflow = Client( Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: InvalidTokenProvider())) let records: [[String: Any]] = [ diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift index 5b9aa5d6..0f1c7e55 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift @@ -12,7 +12,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]! ) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token") ) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index 149cb91c..0253054b 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -15,8 +15,8 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { override func setUp() { self.skyflow = SkyflowFlowVault.initialize( Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG, env: .DEV)) ) diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift index d9030634..504fbc2a 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift index 7d87e07d..8b068e83 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift @@ -14,8 +14,8 @@ class InputFormattingTests: XCTestCase { override func setUp() { self.skyflow = SkyflowFlowVault.initialize( - Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + Configuration(vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider()) ) } diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift index 7571a7de..ce86a83f 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift @@ -17,8 +17,8 @@ class TextFieldDelegateTests: XCTestCase { override func setUp() { self.skyflow = SkyflowFlowVault.initialize( - Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + Configuration(vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider()) ) } diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift index f9ae2008..b95fcdbe 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift index 949cb438..f33d7833 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class ConnectionTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index c5022c3a..f47cfe1f 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -21,8 +21,8 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { override func setUp() { self.skyflow = Client(Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) self.revealTestId = "6255-9119-4502-5915" diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift index 530a2067..50fde75d 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift @@ -9,7 +9,7 @@ import SkyflowCore public class DemoTokenProvider: TokenProvider { public func getBearerToken(_ apiCallback: Callback) { - if let url = URL(string: ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"]!) { + if let url = URL(string: (ProcessInfo.processInfo.environment["TOKEN_ENDPOINT"] ?? "https://dummy.vault.skyflowapis.dev/token")) { let session = URLSession(configuration: .default) let task = session.dataTask(with: url) { data, _, error in if error != nil { diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index 752f6dca..35eea294 100644 --- a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -15,10 +15,10 @@ class skyflow_iOS_revealTests: XCTestCase { override func setUp() { self.skyflow = Client(Configuration( - vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) - self.revealTestId = ProcessInfo.processInfo.environment["DETOKENIZE_TEST_TOKEN"]! + self.revealTestId = (ProcessInfo.processInfo.environment["DETOKENIZE_TEST_TOKEN"] ?? "dummy_detokenize_token") } override func tearDown() { @@ -126,8 +126,8 @@ class skyflow_iOS_revealTests: XCTestCase { } let skyflow = Client( - Configuration(vaultID: ProcessInfo.processInfo.environment["VAULT_ID"]!, - vaultURL: ProcessInfo.processInfo.environment["VAULT_URL"]!, + Configuration(vaultID: (ProcessInfo.processInfo.environment["VAULT_ID"] ?? "dummy_vault_id"), + vaultURL: (ProcessInfo.processInfo.environment["VAULT_URL"] ?? "https://dummy.vault.skyflowapis.dev/"), tokenProvider: InvalidTokenProvider())) let defaultRecords = ["records": [["token": self.revealTestId]]] From 8af969a2ed9b6f755e60efd8bdbefea6a0ee4139 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 14 Aug 2026 20:29:20 +0530 Subject: [PATCH 09/16] SK-3043 fix pod lint issue --- Sources/Skyflow/client/{Client.swift => ClientOperations.swift} | 0 .../client/{Client.swift => ClientOperations.swift} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Sources/Skyflow/client/{Client.swift => ClientOperations.swift} (100%) rename Sources/SkyflowFlowVault/client/{Client.swift => ClientOperations.swift} (100%) diff --git a/Sources/Skyflow/client/Client.swift b/Sources/Skyflow/client/ClientOperations.swift similarity index 100% rename from Sources/Skyflow/client/Client.swift rename to Sources/Skyflow/client/ClientOperations.swift diff --git a/Sources/SkyflowFlowVault/client/Client.swift b/Sources/SkyflowFlowVault/client/ClientOperations.swift similarity index 100% rename from Sources/SkyflowFlowVault/client/Client.swift rename to Sources/SkyflowFlowVault/client/ClientOperations.swift From a2838531dee49342c4aba16109d0b18672ffe0eb Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 17 Aug 2026 11:30:55 +0530 Subject: [PATCH 10/16] SK-3043 update README and samples --- .github/workflows/bump_version.yml | 4 +- .github/workflows/internal_release.yml | 4 +- Package.swift | 31 +- README.md | 2273 +---------------- Samples/README.md | 37 - .../xcshareddata/swiftpm/Package.resolved | 15 - .../ExampleAPICallback.swift | 7 - .../ResponseStructs.swift | 7 - .../ExampleAPICallback.swift | 7 - .../ComposableElements/ResponseStructs.swift | 7 - .../InputFormatting/ExampleAPICallback.swift | 7 - .../InputFormatting/ResponseStructs.swift | 7 - Samples/Skyflow/InputFormatting/Podfile.lock | 16 - .../UpsertFeature/ResponseStructs.swift | 7 - Skyflow.podspec | 6 +- Skyflow/README.md | 2267 ++++++++++++++++ .../project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../CollectAndRevealSample/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../CardBrandChoiceSampleViewController.swift | 2 +- .../CollectAndRevealViewController.swift | 111 +- .../ExampleAPICallback.swift | 48 + .../ExampleTokenProvider.swift | 0 .../CollectAndRevealSample/Info.plist | 0 .../ResponseStructs.swift | 23 + .../SceneDelegate.swift | 0 .../ViewController.swift | 0 .../Samples/CollectAndRevealSample/Podfile | 13 + .../Samples}/CollectAndRevealSample/README.md | 0 .../project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../ComposableElements/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../ExampleAPICallback.swift | 48 + .../ExampleTokenProvider.swift | 0 .../ComposableElements/Info.plist | 0 .../ComposableElements/ResponseStructs.swift | 23 + .../ComposableElements/SceneDelegate.swift | 0 .../ComposableElements/Untitled.swift | 0 .../ComposableElements/ViewController.swift | 104 +- .../ComposableElementsTests.swift | 0 .../ComposableElementsUITests.swift | 0 ...ComposableElementsUITestsLaunchTests.swift | 0 Skyflow/Samples/ComposableElements/Podfile | 20 + .../GetSample.xcodeproj/project.pbxproj | 640 +++++ .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 23 + .../GetSample/GetSample/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../GetSample}/Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../GetSample}/Base.lproj/Main.storyboard | 0 .../GetSample/ExampleAPICallback.swift | 50 + .../GetSample/ExampleTokenProvider.swift | 31 + .../Samples/GetSample/GetSample}/Info.plist | 0 .../GetSample/GetSample/ResponseStructs.swift | 23 + .../GetSample/GetSample/SceneDelegate.swift | 52 + .../GetSample/GetSample/ViewController.swift | 112 + .../GetSampleTests/GetSampleTests.swift | 36 + .../GetSampleUITests/GetSampleUITests.swift | 41 + .../GetSampleUITestsLaunchTests.swift | 32 + Skyflow/Samples/GetSample/Podfile | 12 + .../InputFormatting.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../InputFormatting/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../InputFormatting/ExampleAPICallback.swift | 50 + .../ExampleTokenProvider.swift | 0 .../InputFormatting/Info.plist | 25 + .../InputFormatting/ResponseStructs.swift | 29 + .../InputFormatting/SceneDelegate.swift | 0 .../InputFormatting/ViewController.swift | 60 +- .../InputFormattingTests.swift | 0 .../InputFormattingUITests.swift | 0 .../InputFormattingUITestsLaunchTests.swift | 0 Skyflow/Samples/InputFormatting/Podfile | 21 + {Samples => Skyflow/Samples}/Package.swift | 0 Skyflow/Samples/README.md | 29 + .../Samples/UpdateDataUsingElements/Podfile | 26 + .../project.pbxproj | 756 ++++++ .../contents.xcworkspacedata | 10 + .../UpdateDataUsingElements/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 35 + .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../ExampleAPICallback.swift | 48 + .../ExampleTokenProvider.swift | 31 + .../UpdateDataUsingElements/Info.plist | 25 + .../ResponseStructs.swift | 24 + .../SceneDelegate.swift | 52 + .../ViewController.swift | 183 ++ .../UpdateDataUsingElementsTests.swift | 36 + .../UpdateDataUsingElementsUITests.swift | 41 + ...eDataUsingElementsUITestsLaunchTests.swift | 33 + Skyflow/Samples/UpsertFeature/Podfile | 4 + .../UpsertFeature.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 9 + .../UpsertFeature/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 25 + .../UpsertFeature/Base.lproj/Main.storyboard | 24 + .../UpsertFeature/UpsertFeature/Info.plist | 0 .../UpsertFeature/ResponseStructs.swift | 20 + .../UpsertFeature/SceneDelegate.swift | 0 .../UpsertFeature/ViewController.swift | 73 +- Skyflow/Samples/Validations/Podfile | 12 + .../Samples}/Validations/README.md | 0 .../Validations.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../Validations/Validations/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 0 .../Validations/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Validations/Base.lproj/Main.storyboard | 24 + .../Validations/ExampleAPICallback.swift | 0 .../Validations/ExampleTokenProvider.swift | 0 .../Validations/Validations/Info.plist | 0 .../Validations/SceneDelegate.swift | 0 .../Validations/ViewController.swift | 0 .../Skyflow => Skyflow/Sources}/Exports.swift | 0 .../Skyflow => Skyflow/Sources}/Init.swift | 0 .../Sources}/client/ClientOperations.swift | 0 .../collect/CollectContainerOperations.swift | 0 .../Sources}/collect/CollectRequestBody.swift | 0 .../Sources}/collect/ICOptions.swift | 0 .../ComposableContainerOperations.swift | 0 .../Sources}/models/CollectElementInput.swift | 0 .../models/CollectElementOptions.swift | 0 .../Sources}/models/CollectOptions.swift | 0 .../Sources}/models/Configuration.swift | 0 .../Sources}/models/ContainerOptions.swift | 0 .../Sources}/models/GetByIdRecord.swift | 0 .../Sources}/models/GetOptions.swift | 0 .../Sources}/models/GetRecord.swift | 0 .../Sources}/models/InsertOptions.swift | 0 .../Sources}/models/RevealElementInput.swift | 0 .../models/RevealElementOptions.swift | 0 .../Sources}/models/RevealOptions.swift | 0 .../Sources}/networking/APIClientLegacy.swift | 0 .../networking/CollectAPICallback.swift | 0 .../Sources}/networking/GetAPICallback.swift | 0 .../networking/InsertAPICallback.swift | 0 .../networking/RevealApiCallback.swift | 0 .../networking/RevealByIDAPICallback.swift | 0 .../networking/RevealRequestRecord.swift | 0 .../reveal/RevealContainerOperations.swift | 0 .../Sources}/reveal/RevealRequestBody.swift | 0 .../Sources}/reveal/RevealValueCallback.swift | 0 .../validations/RequestValidators.swift | 0 .../skyflow-iOS-getByIdTests/DemoImpl.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_getByIdTests.swift | 0 .../skyflow_iOS_getByIdUtilTests.swift | 0 .../skyflow-iOS-getTests/DemoImpl.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_getTests.swift | 0 .../skyflow_iOS_getUtilTests.swift | 0 .../skyflow_iOS_validateGetRecordTest.swift | 0 .../CoreRequestValidatorsTests.swift | 0 .../skyflow-iOS-legacyTests/DemoImpl.swift | 0 .../MockURLProtocol.swift | 0 ...w_iOS_dormantCollectApiCallbackTests.swift | 0 ...w_iOS_dormantCollectRequestBodyTests.swift | 0 .../skyflow_iOS_legacyClientApiTests.swift | 0 ...egacyCollectApiCallbackCoverageTests.swift | 0 .../skyflow_iOS_legacyCollectOpsTests.swift | 0 .../skyflow_iOS_legacyRevealOpsTests.swift | 0 .../skyflow_iOS_legacyUtilTests.swift | 0 .../skyflow_iOS_legacyValidatorTests.swift | 0 .../skyflow_iOS_revealApiCallbackTests.swift | 0 .../ClientScenario.swift | 0 .../skyflow-iOS-scenarioTests/DemoImpl.swift | 0 .../DetokenizeScenario.swift | 0 .../GetByIdScenario.swift | 0 .../InsertScenario.swift | 0 .../skyflow-iOS-scenarioTests/TestData.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_detokenizeScenarioTests.swift | 0 .../skyflow_iOS_getByIdScenarioTests.swift | 0 .../skyflow_iOS_insertScenarioTests.swift | 0 .../Amex-Card.imageset/Amex-Card@3x.png | Bin .../Amex-Card.imageset/Contents.json | 0 .../Cartes-Bancaires-Card.png | Bin .../Contents.json | 0 .../Resources/Assets.xcassets/Contents.json | 6 + .../Copy-Icon.imageset/Contents.json | 0 .../Copy-Icon.imageset/Copy-Icon.png | Bin .../Diners-Card.imageset/Contents.json | 0 .../Diners-Card.imageset/Diners-Card@3x.png | Bin .../Discover-Card.imageset/Contents.json | 0 .../Discover-Card@3x.png | Bin .../Hipercard-Card.imageset/Contents.json | 0 .../Hipercard-Card@3x.png | Bin .../JCB-Card.imageset/Contents.json | 0 .../JCB-Card.imageset/JCB-Card@3x.png | Bin .../Maestro-Card.imageset/Contents.json | 0 .../Maestro-Card.imageset/Maestro-Card@3x.png | Bin .../Mastercard-Card.imageset/Contents.json | 0 .../Mastercard-Card@3x.png | Bin .../Success-Icon.imageset/Contents.json | 0 .../Success-Icon.imageset/Success-Icon.png | Bin .../Unionpay-Card.imageset/Contents.json | 0 .../Unionpay-Card@3x.png | Bin .../Unknown-Card.imageset/Contents.json | 0 .../Unknown-Card.imageset/Unknown-Card@3x.png | Bin .../Visa-Card.imageset/Contents.json | 0 .../Visa-Card.imageset/Group 82.png | Bin .../checkmark.imageset/Contents.json | 0 .../checkmark.imageset/checkmark.png | Bin .../dropdown.imageset/Contents.json | 0 .../dropdown.imageset/dropdown.png | Bin .../Sources}/Version.swift | 0 .../Sources}/client/Client.swift | 0 .../Sources}/client/Env.swift | 0 .../Sources}/client/Options.swift | 0 .../Sources}/collect/CollectContainer.swift | 0 .../collect/elements/BaseElement.swift | 0 .../Sources}/collect/elements/Element.swift | 0 .../collect/elements/ElementType.swift | 0 .../collect/elements/FormatTextField.swift | 0 .../collect/elements/PaddingLabel.swift | 0 .../collect/elements/SkyflowElement.swift | 0 .../Sources}/collect/elements/TextField.swift | 0 .../TextFieldValidationDelegate.swift | 0 .../collect/elements/state/State.swift | 0 .../collect/elements/state/StateforText.swift | 0 .../collect/elements/styles/Style.swift | 0 .../collect/elements/styles/Styles.swift | 0 .../composable/BaseContainerOptions.swift | 0 .../composable/ComposableContainer.swift | 0 .../Sources}/containers/Container.swift | 0 .../containers/ContainerProtocol.swift | 0 .../Sources}/containers/ContainerType.swift | 0 .../Sources}/errors/ErrorCodes.swift | 0 .../Sources}/logging/ContextOptions.swift | 0 .../Sources}/logging/EventName.swift | 0 .../Sources}/logging/InterfaceName.swift | 0 .../Sources}/logging/Log.swift | 0 .../Sources}/logging/LogLevel.swift | 0 .../Sources}/logging/Message.swift | 0 .../Sources}/models/AutoshiftAllowed.swift | 0 .../models/BaseCollectElementInput.swift | 0 .../models/BaseCollectElementOptions.swift | 0 .../Sources}/models/BaseConfiguration.swift | 0 .../models/BaseRevealElementInput.swift | 0 .../models/BaseRevealElementOptions.swift | 0 .../Sources}/models/RedactionType.swift | 0 .../Sources}/networking/APIClient.swift | 0 .../networking/TokenAPICallback.swift | 0 .../Sources}/protocols/Callback.swift | 0 .../Sources}/protocols/TokenProvider.swift | 0 .../Sources}/reveal/FormatLabel.swift | 0 .../Sources}/reveal/Label.swift | 0 .../Sources}/reveal/RevealContainer.swift | 0 .../Sources}/reveal/RevealErrorRecord.swift | 0 .../Sources}/reveal/RevealSuccessRecord.swift | 0 .../Sources}/reveal/SkyflowLabelView.swift | 0 .../Sources}/reveal/UILabel+Copyable.swift | 0 .../Sources}/utils/CardBin.swift | 0 .../Sources}/utils/CardType.swift | 0 .../Sources}/utils/CharacterSet.swift | 0 .../Sources}/utils/ConversionHelpers.swift | 0 .../Sources}/utils/DictionaryExtension.swift | 0 .../Sources}/utils/FetchMetrices.swift | 0 .../Sources}/utils/SkyflowAssetBundle.swift | 0 .../Sources}/utils/StringExtension.swift | 0 .../validations/CoreRequestValidators.swift | 0 .../validations/ElementValueMatchRule.swift | 0 .../validations/LengthMatchRule.swift | 0 .../Sources}/validations/RegexMatchRule.swift | 0 .../SkyflowInternalValidatorProtocol.swift | 0 .../SkyflowValidateCardExpirationDate.swift | 0 .../SkyflowValidateCardNumber.swift | 0 .../SkyflowValidateExpirationMonth.swift | 0 .../SkyflowValidateExpirationYear.swift | 0 .../SkyflowValidateLengthMatch.swift | 0 .../validations/SkyflowValidationError.swift | 0 .../validations/SkyflowValidator.swift | 0 .../Sources}/validations/ValidationRule.swift | 0 .../Sources}/validations/ValidationSet.swift | 0 SkyflowFlowVault.podspec | 6 +- SkyflowFlowVault/README.md | 2219 ++++++++++++++++ .../debug/index/db/v13/p5445--a626e6/data.mdb | Bin 0 -> 67108864 bytes .../debug/index/db/v13/p5445--a626e6/lock.mdb | Bin 0 -> 8256 bytes .../project.pbxproj | 454 ++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../CollectAndRevealSample/AppDelegate.swift | 27 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../CardBrandChoiceSampleViewController.swift | 147 ++ .../CollectAndRevealViewController.swift | 259 ++ .../ExampleAPICallback.swift | 48 + .../ExampleTokenProvider.swift | 31 + .../CollectAndRevealSample/Info.plist | 71 + .../ResponseStructs.swift | 23 + .../SceneDelegate.swift | 44 + .../ViewController.swift | 83 + .../Samples}/CollectAndRevealSample/Podfile | 3 +- .../Samples/CollectAndRevealSample/README.md | 1 + .../project.pbxproj | 601 +++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ComposableElements/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../ExampleAPICallback.swift | 48 + .../ExampleTokenProvider.swift | 31 + .../ComposableElements/Info.plist | 25 + .../ComposableElements/ResponseStructs.swift | 23 + .../ComposableElements/SceneDelegate.swift | 52 + .../ComposableElements/Untitled.swift | 0 .../ComposableElements/ViewController.swift | 276 ++ .../ComposableElementsTests.swift | 36 + .../ComposableElementsUITests.swift | 41 + ...ComposableElementsUITestsLaunchTests.swift | 32 + .../Samples}/ComposableElements/Podfile | 3 +- .../InputFormatting.xcodeproj/project.pbxproj | 779 ++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../InputFormatting/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../InputFormatting/ExampleAPICallback.swift | 50 + .../ExampleTokenProvider.swift | 34 + .../InputFormatting/Info.plist | 25 + .../InputFormatting/ResponseStructs.swift | 29 + .../InputFormatting/SceneDelegate.swift | 51 + .../InputFormatting/ViewController.swift | 324 +++ .../InputFormattingTests.swift | 36 + .../InputFormattingUITests.swift | 41 + .../InputFormattingUITestsLaunchTests.swift | 32 + .../Samples}/InputFormatting/Podfile | 3 +- SkyflowFlowVault/Samples/Package.swift | 8 + SkyflowFlowVault/Samples/README.md | 30 + .../Samples/UpdateDataUsingElements/Podfile | 26 + .../project.pbxproj | 756 ++++++ .../contents.xcworkspacedata | 10 + .../UpdateDataUsingElements/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../ExampleAPICallback.swift | 48 + .../ExampleTokenProvider.swift | 31 + .../UpdateDataUsingElements/Info.plist | 25 + .../ResponseStructs.swift | 24 + .../SceneDelegate.swift | 52 + .../ViewController.swift | 183 ++ .../UpdateDataUsingElementsTests.swift | 36 + .../UpdateDataUsingElementsUITests.swift | 41 + ...eDataUsingElementsUITestsLaunchTests.swift | 33 + .../Samples/UpsertFeature/Podfile | 4 + .../UpsertFeature.xcodeproj/project.pbxproj | 383 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 25 + .../UpsertFeature/AppDelegate.swift | 30 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../UpsertFeature/Base.lproj/Main.storyboard | 24 + .../UpsertFeature/UpsertFeature/Info.plist | 66 + .../UpsertFeature/ResponseStructs.swift | 20 + .../UpsertFeature/SceneDelegate.swift | 47 + .../UpsertFeature/ViewController.swift | 227 ++ .../Samples}/Validations/Podfile | 3 +- .../Samples/Validations/README.md | 1 + .../Validations.xcodeproj/project.pbxproj | 344 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../Validations/Validations/AppDelegate.swift | 34 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Validations/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Validations/Base.lproj/Main.storyboard | 24 + .../Validations/ExampleAPICallback.swift | 26 + .../Validations/ExampleTokenProvider.swift | 38 + .../Validations/Validations/Info.plist | 66 + .../Validations/SceneDelegate.swift | 51 + .../Validations/ViewController.swift | 140 + .../Sources}/Exports.swift | 0 .../Sources}/Init.swift | 0 .../Sources}/client/ClientOperations.swift | 0 .../Sources}/collect/CollectCallback.swift | 0 .../collect/CollectContainerOperations.swift | 0 .../collect/CollectRequestBuilder.swift | 0 .../ComposableContainerOperations.swift | 0 .../composable/ContainerOptions.swift | 0 .../Sources}/models/AdditionalFields.swift | 0 .../Sources}/models/CollectElementInput.swift | 0 .../models/CollectElementOptions.swift | 0 .../Sources}/models/CollectOptions.swift | 0 .../Sources}/models/CollectResponse.swift | 0 .../Sources}/models/Configuration.swift | 0 .../Sources}/models/InsertOptions.swift | 0 .../Sources}/models/RevealElementInput.swift | 0 .../models/RevealElementOptions.swift | 0 .../Sources}/models/RevealMetadata.swift | 0 .../Sources}/models/RevealOptions.swift | 0 .../Sources}/models/RevealRequestRecord.swift | 0 .../Sources}/models/RevealResponse.swift | 0 .../Sources}/models/Token.swift | 0 .../Sources}/models/TokenGroupRedaction.swift | 0 .../Sources}/models/UpsertOption.swift | 0 .../networking/APIClientFlowVault.swift | 0 .../FlowVaultCollectAPICallback.swift | 0 .../FlowVaultRevealApiCallback.swift | 0 .../Sources}/reveal/RevealCallback.swift | 0 .../reveal/RevealContainerOperations.swift | 0 .../reveal/RevealRequestBuilder.swift | 0 .../Sources}/reveal/RevealValueCallback.swift | 0 .../Sources}/utils/CVVTokenReplacer.swift | 0 .../validations/RequestValidators.swift | 0 .../skyflow-iOS-collectTests/DemoImpl.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_collectTests.swift | 0 ...ow_iOS_emptySkyflowIdValidationTests.swift | 0 .../DemoImpl.swift | 0 ...skyflow_iOS_composableEelementsTests.swift | 0 .../skyflow_iOS_composableFlowTests.swift | 0 .../CollectElementOptionsTests.swift | 0 .../skyflow-iOS-elementTests/DemoImpl.swift | 0 .../InputFormattingTests.swift | 0 .../TextFieldDelegateTests.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_containerTests.swift | 0 .../skyflow_iOS_elementTests.swift | 0 .../skyflow-iOS-errorTests/DemoImpl.swift | 0 .../skyflow-iOS-errorTests/GatewayImpl.swift | 0 .../Skyflow_iOS_collectErrorTests.swift | 0 .../Skyflow_iOS_generalErrorTests.swift | 0 .../Skyflow_iOS_revealErrorTests.swift | 0 ...low_iOS_revealOptionsValidationTests.swift | 0 .../XCTestManifests.swift | 0 .../skyflow-iOS-revealTests/DemoImpl.swift | 0 .../RevealElementOptionsTests.swift | 0 .../RevealSkyflowLabelViewTests.swift | 0 .../XCTestManifests.swift | 0 .../skyflow_iOS_revealTests.swift | 0 .../skyflow-iOS-utilTests/DemoImpl.swift | 0 .../MockURLProtocol.swift | 0 .../TokenLifecycleTests.swift | 0 .../TokenParsingTests.swift | 0 .../skyflow_iOS_collectUtilTests.swift | 0 .../skyflow_iOS_cvvMockTests.swift | 0 .../skyflow_iOS_flowVaultSdkTests.swift | 0 .../skyflow_iOS_insertUtilTests.swift | 0 .../skyflow_iOS_revealUtilTests.swift | 0 .../skyflow_iOS_utilTests.swift | 0 scripts/bump_version.sh | 2 +- 497 files changed, 15127 insertions(+), 2651 deletions(-) delete mode 100644 Samples/README.md delete mode 100644 Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift delete mode 100644 Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift delete mode 100644 Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift delete mode 100644 Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift delete mode 100644 Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift delete mode 100644 Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift delete mode 100644 Samples/Skyflow/InputFormatting/Podfile.lock delete mode 100644 Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift create mode 100644 Skyflow/README.md rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift (97%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift (68%) create mode 100644 Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/Info.plist (100%) create mode 100644 Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift (100%) create mode 100644 Skyflow/Samples/CollectAndRevealSample/Podfile rename {Samples/Skyflow => Skyflow/Samples}/CollectAndRevealSample/README.md (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements.xcodeproj/project.pbxproj (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/AppDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Assets.xcassets/Contents.json (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Base.lproj/Main.storyboard (100%) create mode 100644 Skyflow/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/ExampleTokenProvider.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Info.plist (100%) create mode 100644 Skyflow/Samples/ComposableElements/ComposableElements/ResponseStructs.swift rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/SceneDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/Untitled.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElements/ViewController.swift (68%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift (100%) create mode 100644 Skyflow/Samples/ComposableElements/Podfile create mode 100644 Skyflow/Samples/GetSample/GetSample.xcodeproj/project.pbxproj rename {Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj => Skyflow/Samples/GetSample/GetSample.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj => Skyflow/Samples/GetSample/GetSample.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Skyflow/Samples/GetSample/GetSample/AppDelegate.swift rename {Samples/Skyflow/InputFormatting/InputFormatting => Skyflow/Samples/GetSample/GetSample}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {Samples/Skyflow/InputFormatting/InputFormatting => Skyflow/Samples/GetSample/GetSample}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Samples/Skyflow/InputFormatting/InputFormatting => Skyflow/Samples/GetSample/GetSample}/Assets.xcassets/Contents.json (100%) rename {Samples/Skyflow/InputFormatting/InputFormatting => Skyflow/Samples/GetSample/GetSample}/Base.lproj/LaunchScreen.storyboard (100%) rename {Samples/Skyflow/InputFormatting/InputFormatting => Skyflow/Samples/GetSample/GetSample}/Base.lproj/Main.storyboard (100%) create mode 100644 Skyflow/Samples/GetSample/GetSample/ExampleAPICallback.swift create mode 100644 Skyflow/Samples/GetSample/GetSample/ExampleTokenProvider.swift rename {Samples/Skyflow/InputFormatting/InputFormatting => Skyflow/Samples/GetSample/GetSample}/Info.plist (100%) create mode 100644 Skyflow/Samples/GetSample/GetSample/ResponseStructs.swift create mode 100644 Skyflow/Samples/GetSample/GetSample/SceneDelegate.swift create mode 100644 Skyflow/Samples/GetSample/GetSample/ViewController.swift create mode 100644 Skyflow/Samples/GetSample/GetSampleTests/GetSampleTests.swift create mode 100644 Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift create mode 100644 Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift create mode 100644 Skyflow/Samples/GetSample/Podfile rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormatting.xcodeproj/project.pbxproj (100%) rename {Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj => Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace => Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace => Skyflow/Samples/InputFormatting/InputFormatting.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormatting/AppDelegate.swift (100%) rename {Samples/Skyflow/UpsertFeature/UpsertFeature => Skyflow/Samples/InputFormatting/InputFormatting}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) create mode 100644 Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json rename {Samples/Skyflow/UpsertFeature/UpsertFeature => Skyflow/Samples/InputFormatting/InputFormatting}/Assets.xcassets/Contents.json (100%) rename {Samples/Skyflow/UpsertFeature/UpsertFeature => Skyflow/Samples/InputFormatting/InputFormatting}/Base.lproj/LaunchScreen.storyboard (100%) rename {Samples/Skyflow/UpsertFeature/UpsertFeature => Skyflow/Samples/InputFormatting/InputFormatting}/Base.lproj/Main.storyboard (100%) create mode 100644 Skyflow/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormatting/ExampleTokenProvider.swift (100%) create mode 100644 Skyflow/Samples/InputFormatting/InputFormatting/Info.plist create mode 100644 Skyflow/Samples/InputFormatting/InputFormatting/ResponseStructs.swift rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormatting/SceneDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormatting/ViewController.swift (86%) rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormattingTests/InputFormattingTests.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift (100%) create mode 100644 Skyflow/Samples/InputFormatting/Podfile rename {Samples => Skyflow/Samples}/Package.swift (100%) create mode 100644 Skyflow/Samples/README.md create mode 100644 Skyflow/Samples/UpdateDataUsingElements/Podfile create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift rename {Samples/Skyflow/Validations/Validations => Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json rename {Samples/Skyflow/Validations/Validations => Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements}/Assets.xcassets/Contents.json (100%) rename {Samples/Skyflow/Validations/Validations => Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements}/Base.lproj/LaunchScreen.storyboard (100%) rename {Samples/Skyflow/Validations/Validations => Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements}/Base.lproj/Main.storyboard (100%) create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift create mode 100644 Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift create mode 100644 Skyflow/Samples/UpsertFeature/Podfile rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj (100%) rename {Samples/Skyflow/Validations/Validations.xcodeproj => Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename {Samples/Skyflow/Validations/Validations.xcodeproj => Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (55%) rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature/AppDelegate.swift (100%) create mode 100644 Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Sources/SkyflowCore/Resources => Skyflow/Samples/UpsertFeature/UpsertFeature}/Assets.xcassets/Contents.json (100%) create mode 100644 Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard create mode 100644 Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature/Info.plist (100%) create mode 100644 Skyflow/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature/SceneDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/UpsertFeature/UpsertFeature/ViewController.swift (72%) create mode 100644 Skyflow/Samples/Validations/Podfile rename {Samples/Skyflow => Skyflow/Samples}/Validations/README.md (100%) rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations.xcodeproj/project.pbxproj (100%) create mode 100644 Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/AppDelegate.swift (100%) create mode 100644 Skyflow/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) create mode 100644 Skyflow/Samples/Validations/Validations/Assets.xcassets/Contents.json create mode 100644 Skyflow/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard create mode 100644 Skyflow/Samples/Validations/Validations/Base.lproj/Main.storyboard rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/ExampleAPICallback.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/ExampleTokenProvider.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/Info.plist (100%) rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/SceneDelegate.swift (100%) rename {Samples/Skyflow => Skyflow/Samples}/Validations/Validations/ViewController.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/Exports.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/Init.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/client/ClientOperations.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/collect/CollectContainerOperations.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/collect/CollectRequestBody.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/collect/ICOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/composable/ComposableContainerOperations.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/CollectElementInput.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/CollectElementOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/CollectOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/Configuration.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/ContainerOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/GetByIdRecord.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/GetOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/GetRecord.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/InsertOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/RevealElementInput.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/RevealElementOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/models/RevealOptions.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/APIClientLegacy.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/CollectAPICallback.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/GetAPICallback.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/InsertAPICallback.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/RevealApiCallback.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/RevealByIDAPICallback.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/networking/RevealRequestRecord.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/reveal/RevealContainerOperations.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/reveal/RevealRequestBody.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/reveal/RevealValueCallback.swift (100%) rename {Sources/Skyflow => Skyflow/Sources}/validations/RequestValidators.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getByIdTests/DemoImpl.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getByIdTests/XCTestManifests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getTests/DemoImpl.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getTests/XCTestManifests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getTests/skyflow_iOS_getTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/DemoImpl.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/MockURLProtocol.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/ClientScenario.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/DemoImpl.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/DetokenizeScenario.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/GetByIdScenario.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/InsertScenario.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/TestData.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/XCTestManifests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift (100%) rename {Tests/Skyflow => Skyflow/Tests}/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json (100%) create mode 100644 SkyflowCore/Sources/Resources/Assets.xcassets/Contents.json rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/checkmark.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/checkmark.imageset/checkmark.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/dropdown.imageset/Contents.json (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Resources/Assets.xcassets/dropdown.imageset/dropdown.png (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/Version.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/client/Client.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/client/Env.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/client/Options.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/CollectContainer.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/BaseElement.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/Element.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/ElementType.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/FormatTextField.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/PaddingLabel.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/SkyflowElement.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/TextField.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/TextFieldValidationDelegate.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/state/State.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/state/StateforText.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/styles/Style.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/collect/elements/styles/Styles.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/composable/BaseContainerOptions.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/composable/ComposableContainer.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/containers/Container.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/containers/ContainerProtocol.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/containers/ContainerType.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/errors/ErrorCodes.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/logging/ContextOptions.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/logging/EventName.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/logging/InterfaceName.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/logging/Log.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/logging/LogLevel.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/logging/Message.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/AutoshiftAllowed.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/BaseCollectElementInput.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/BaseCollectElementOptions.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/BaseConfiguration.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/BaseRevealElementInput.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/BaseRevealElementOptions.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/models/RedactionType.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/networking/APIClient.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/networking/TokenAPICallback.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/protocols/Callback.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/protocols/TokenProvider.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/FormatLabel.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/Label.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/RevealContainer.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/RevealErrorRecord.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/RevealSuccessRecord.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/SkyflowLabelView.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/reveal/UILabel+Copyable.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/CardBin.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/CardType.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/CharacterSet.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/ConversionHelpers.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/DictionaryExtension.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/FetchMetrices.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/SkyflowAssetBundle.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/utils/StringExtension.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/CoreRequestValidators.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/ElementValueMatchRule.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/LengthMatchRule.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/RegexMatchRule.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowInternalValidatorProtocol.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidateCardExpirationDate.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidateCardNumber.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidateExpirationMonth.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidateExpirationYear.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidateLengthMatch.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidationError.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/SkyflowValidator.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/ValidationRule.swift (100%) rename {Sources/SkyflowCore => SkyflowCore/Sources}/validations/ValidationSet.swift (100%) create mode 100644 SkyflowFlowVault/README.md create mode 100644 SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/data.mdb create mode 100644 SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/lock.mdb create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift rename {Samples/Skyflow => SkyflowFlowVault/Samples}/CollectAndRevealSample/Podfile (83%) create mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/README.md create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/AppDelegate.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Info.plist create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/SceneDelegate.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Untitled.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift create mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift rename {Samples/Skyflow => SkyflowFlowVault/Samples}/ComposableElements/Podfile (89%) create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/AppDelegate.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Info.plist create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/SceneDelegate.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift create mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift rename {Samples/Skyflow => SkyflowFlowVault/Samples}/InputFormatting/Podfile (89%) create mode 100644 SkyflowFlowVault/Samples/Package.swift create mode 100644 SkyflowFlowVault/Samples/README.md create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/Podfile create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift create mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/Podfile create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Info.plist create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift rename {Samples/Skyflow => SkyflowFlowVault/Samples}/Validations/Podfile (82%) create mode 100644 SkyflowFlowVault/Samples/Validations/README.md create mode 100644 SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj create mode 100644 SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/AppDelegate.swift create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/Contents.json create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/Main.storyboard create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/ExampleTokenProvider.swift create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/Info.plist create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/SceneDelegate.swift create mode 100644 SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/Exports.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/Init.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/client/ClientOperations.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/collect/CollectCallback.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/collect/CollectContainerOperations.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/collect/CollectRequestBuilder.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/composable/ComposableContainerOperations.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/composable/ContainerOptions.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/AdditionalFields.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/CollectElementInput.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/CollectElementOptions.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/CollectOptions.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/CollectResponse.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/Configuration.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/InsertOptions.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/RevealElementInput.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/RevealElementOptions.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/RevealMetadata.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/RevealOptions.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/RevealRequestRecord.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/RevealResponse.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/Token.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/TokenGroupRedaction.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/models/UpsertOption.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/networking/APIClientFlowVault.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/networking/FlowVaultCollectAPICallback.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/networking/FlowVaultRevealApiCallback.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/reveal/RevealCallback.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/reveal/RevealContainerOperations.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/reveal/RevealRequestBuilder.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/reveal/RevealValueCallback.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/utils/CVVTokenReplacer.swift (100%) rename {Sources/SkyflowFlowVault => SkyflowFlowVault/Sources}/validations/RequestValidators.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-collectTests/DemoImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-collectTests/XCTestManifests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-composableTests/DemoImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/CollectElementOptionsTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/DemoImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/InputFormattingTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/TextFieldDelegateTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/XCTestManifests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/DemoImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/GatewayImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-errorTests/XCTestManifests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-revealTests/DemoImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-revealTests/RevealElementOptionsTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-revealTests/XCTestManifests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/DemoImpl.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/MockURLProtocol.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/TokenLifecycleTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/TokenParsingTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift (100%) rename {Tests/SkyflowFlowVault => SkyflowFlowVault/Tests}/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift (100%) diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 4058a573..73e48e9c 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -8,7 +8,7 @@ on: paths-ignore: - 'Skyflow.podspec' - '*.md' - - 'Sources/SkyflowCore/Version.swift' + - 'SkyflowCore/Sources/Version.swift' branches: - public-release/* @@ -42,6 +42,6 @@ jobs: git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com git add Skyflow.podspec SkyflowFlowVault.podspec - git add Sources/SkyflowCore/Version.swift + git add SkyflowCore/Sources/Version.swift git commit -m "[AUTOMATED] Public Release ${{ steps.version.outputs.version }}" git push origin \ No newline at end of file diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index fe351a81..25c168f5 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -9,7 +9,7 @@ on: - 'Skyflow.podspec' - 'SkyflowFlowVault.podspec' - '*.md' - - 'Sources/SkyflowCore/Version.swift' + - 'SkyflowCore/Sources/Version.swift' branches: - release/* @@ -38,7 +38,7 @@ jobs: git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com git add Skyflow.podspec SkyflowFlowVault.podspec - git add Sources/SkyflowCore/Version.swift + git add SkyflowCore/Sources/Version.swift git commit -m "[AUTOMATED] Private Release ${{ steps.previoustag.outputs.tag }}.$(git rev-parse --short $GITHUB_SHA)" git push origin diff --git a/Package.swift b/Package.swift index 237fe023..4725c3d1 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,7 @@ let package = Package( .target( name: "SkyflowCore", dependencies: [], + path: "SkyflowCore/Sources", resources: [ .process("Resources") ] @@ -33,45 +34,47 @@ let package = Package( // Legacy (v1) contract layer. .target( name: "Skyflow", - dependencies: ["SkyflowCore"] + dependencies: ["SkyflowCore"], + path: "Skyflow/Sources" ), // FlowVault (v2) contract layer. .target( name: "SkyflowFlowVault", - dependencies: ["SkyflowCore"] + dependencies: ["SkyflowCore"], + path: "SkyflowFlowVault/Sources" ), - // Tests for the legacy (v1) SDK live under Tests/Skyflow/, - // FlowVault (v2) SDK tests under Tests/SkyflowFlowVault/. + // Tests for the legacy (v1) SDK live under Skyflow/Tests/, + // FlowVault (v2) SDK tests under SkyflowFlowVault/Tests/. .testTarget( name: "skyflow-iOS-collectTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "Tests/SkyflowFlowVault/skyflow-iOS-collectTests"), + path: "SkyflowFlowVault/Tests/skyflow-iOS-collectTests"), .testTarget(name: "skyflow-iOS-revealTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "Tests/SkyflowFlowVault/skyflow-iOS-revealTests"), + path: "SkyflowFlowVault/Tests/skyflow-iOS-revealTests"), .testTarget(name: "skyflow-iOS-errorTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "Tests/SkyflowFlowVault/skyflow-iOS-errorTests"), + path: "SkyflowFlowVault/Tests/skyflow-iOS-errorTests"), .testTarget(name: "skyflow-iOS-getByIdTests", dependencies: ["Skyflow", "SkyflowCore"], - path: "Tests/Skyflow/skyflow-iOS-getByIdTests"), + path: "Skyflow/Tests/skyflow-iOS-getByIdTests"), .testTarget(name: "skyflow-iOS-elementTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "Tests/SkyflowFlowVault/skyflow-iOS-elementTests"), + path: "SkyflowFlowVault/Tests/skyflow-iOS-elementTests"), .testTarget(name: "skyflow-iOS-utilTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "Tests/SkyflowFlowVault/skyflow-iOS-utilTests"), + path: "SkyflowFlowVault/Tests/skyflow-iOS-utilTests"), .testTarget(name: "skyflow-iOS-legacyTests", dependencies: ["Skyflow", "SkyflowCore"], - path: "Tests/Skyflow/skyflow-iOS-legacyTests"), + path: "Skyflow/Tests/skyflow-iOS-legacyTests"), .testTarget(name: "skyflow-iOS-scenarioTests", dependencies: ["Skyflow", "SkyflowCore"], - path: "Tests/Skyflow/skyflow-iOS-scenarioTests"), + path: "Skyflow/Tests/skyflow-iOS-scenarioTests"), .testTarget(name: "skyflow-iOS-getTests", dependencies: ["Skyflow", "SkyflowCore"], - path: "Tests/Skyflow/skyflow-iOS-getTests"), + path: "Skyflow/Tests/skyflow-iOS-getTests"), .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "Tests/SkyflowFlowVault/skyflow-iOS-composableTests") + path: "SkyflowFlowVault/Tests/skyflow-iOS-composableTests") ] ) diff --git a/README.md b/README.md index 2f68ddbe..557b50ee 100644 --- a/README.md +++ b/README.md @@ -1,2267 +1,28 @@ # skyflow-iOS ---- -Skyflow’s iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. - + +Skyflow's iOS SDKs let you securely collect, tokenize, and display sensitive data in your mobile app without exposing your front-end infrastructure to sensitive data. + [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-ios/actions) [![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-ios.svg)](https://github.com/skyflowapi/skyflow-ios/releases) [![License](https://img.shields.io/github/license/skyflowapi/skyflow-ios)](https://github.com/skyflowapi/skyflow-ios/blob/main/LICENSE) - -# Table of Contents -- [Installation](#installation) - - [Requirements](#requirements) - - [Configuration](#configuration) -- [Initializing Skyflow-iOS](#initializing-skyflow-ios) -- [Securely collecting data client-side](#securely-collecting-data-client-side) -- [Securely collecting data client-side using Composable Elements](#securely-collecting-data-client-side-using-composable-elements) -- [Securely revealing data client-side](#securely-revealing-data-client-side) - -# Installation - -## Requirements -- iOS 15.0 and above - -## Configuration ---- -### SPM (Swift Package Manager) -- Go to File -> Swift Packages -> New Package Dependency (in Xcode IDE) -- Enter https://github.com/skyflowapi/skyflow-iOS.git and press ok. - -### Cocoapods -- To integrate skyflow-iOS into your Xcode project using CocoaPods, specify it in your Podfile: - ``` - #Mentioning the below source will pick the podspec from Skyflow repo - #Otherwise you can add cocoapod trunk as the source - #source 'https://github.com/skyflowapi/skyflow-iOS-spec.git' - - pod 'Skyflow' - ``` - - -# Initializing skyflow-iOS ----- -Use the ```initialize()``` method to initialize a Skyflow client as shown below. -```swift -//DemoTokenProvider is an implementation of the Skyflow.TokenProvider protocol -let demoTokenProvider = DemoTokenProvider() - -let config = Skyflow.Configuration( - vaultID: , - vaultURL: , - tokenProvider: demoTokenProvider, - options: Skyflow.Options( - logLevel: Skyflow.LogLevel, // optional, if not specified default is ERROR - env: Skyflow.Env // optional, if not specified default is PROD - ) -) - -let skyflowClient = Skyflow.initialize(config) -``` - -For the tokenProvider parameter, pass in an implementation of the `Skyflow.TokenProvider` protocol that declares a getBearerToken method which retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. - -For example, if the response of the consumer tokenAPI is in the below format - -``` -{ - "accessToken": string, - "tokenType": string -} -``` - -then, your Skyflow.TokenProvider Implementation should be as below - -```swift -public class DemoTokenProvider: Skyflow.TokenProvider { - public func getBearerToken(_ apiCallback: Skyflow.Callback) { - if let url = URL(string: ) { - let session = URLSession(configuration: .default) - let task = session.dataTask(with: url) {data, _, error in - if error != nil { - print(error!) - return - } - if let safeData = data { - do { - let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] - if let accessToken = x?["accessToken"] { - apiCallback.onSuccess(accessToken) - } - } catch { - apiCallback.onFailure(error) - } - } - } - task.resume() - } - } -} -``` - -NOTE: You should pass access token as `String` value in the success callback of getBearerToken. - -For `logLevel` parameter, there are 4 accepted values in Skyflow.LogLevel - -- `DEBUG` - - When `Skyflow.LogLevel.DEBUG` is passed, all level of logs will be printed(DEBUG, INFO, WARN, ERROR). - -- `INFO` - - When `Skyflow.LogLevel.INFO` is passed, INFO logs for every event that has occurred during the SDK flow execution will be printed along with WARN and ERROR logs. - -- `WARN` - - When `Skyflow.LogLevel.WARN` is passed, WARN and ERROR logs will be printed. - -- `ERROR` - - When `Skyflow.LogLevel.ERROR` is passed, only ERROR logs will be printed. - -`Note`: - - The ranking of logging levels is as follows : DEBUG < INFO < WARN < ERROR - - since `logLevel` is optional, by default the logLevel will be `ERROR`. - - -For `env` parameter, there are 2 accepted values in Skyflow.Env - -- `PROD` -- `DEV` - - In [Event Listeners](#event-listener-on-collect-elements), actual value of element can only be accessed inside the handler when the `env` is set to `DEV`. - -`Note`: - - since `env` is optional, by default the env will be `PROD`. - - Use `env` option with caution, make sure the env is set to `PROD` when using `skyflow-iOS` in production. - - ---- -# Securely collecting data client-side -- [**Inserting data into the vault**](#insert-data-into-the-vault) -- [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) -- [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) -- [**Using validations on Collect Elements**](#validations) -- [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) -- [**UI Error for Collect Elements**](#ui-error-for-collect-elements) -- [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) - -## Insert data into the vault - - To insert data into your vault, use the `skyflowClient.insert()` method. - ```swift - insert(records: [String: Any], options: InsertOptions?= InsertOptions() , callback: Skyflow.Callback) - ``` - The `insert()` method requires a records parameter. - The `records` parameter takes an array of records to insert into the vault. - -#### Insert call example - ```swift - let insertCallback = InsertCallback() //A Custom callback using Skyflow.Callback - skyflowClient.insert( - records: [ - "records": [ - [ - "table": "cards", - "fields": [ - "cardNumber": "41111111111", - "cvv": "123", - ] - ] - ] - ], - callback: insertCallback -) -``` -Skyflow returns tokens for the record you just inserted. -```json -{ - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f37186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - }] -} -``` - - The `options` parameter takes a Skyflow.InsertOptions object. - - InsertOptions includes a `tokens` boolean that controls whether you receive tokens after inserting a record into the vault. - -InsertOptions also supports the **upsert** feature, that lets you conditionally insert or update an existing record by specifying the unique column to use as the unique value. - -For example, if you specify the ‘customer_id’ column to use for upsert, and the same customer_id already exists, the existing record will update. If the customer_id doesn't exist, the vault creates a new record. - -#### Insert call example -```swift -let records = [ - "records" : [ - [ - "table": "customers", //The table where you are inserting the record. - "fields": [ - "name" : "Francis", - "customer_id" : "12345" - ] - ] - ] -] - -let upsertOptions = [["table": "customers", "column": "customer_id"]] as [[String : Any]] -let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) -let insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.Callback - -skyflowClient.insert(records: records, options: insertOptions, callback: insertCallback) -``` - -## Using Skyflow Elements to collect data - -**Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements in your application. - -### Step 1: Create a container - -First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below - -```swift -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) -``` - -### Step 2: Create a collect Element -To create a collect Element, we must first construct a Skyflow.CollectElementInput object defined as shown below: - -```swift -let collectElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element - type: Skyflow.ElementType, // Skyflow.ElementType enum -) -``` -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note**: -- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) -- `table` and `column` are optional only if the element is being used in invokeConnection() - -The `inputStyles` parameter accepts a Skyflow.Styles object which consists of multiple `Skyflow.Styles` objects which should be applied to the form element in the following states: - -- `base`: all other variants inherit from these styles -- `complete`: applied when the Element has valid input -- `empty`: applied when the Element has no input -- `focus`: applied when the Element has focus -- `invalid`: applied when the Element has invalid input - -Each Style object accepts the following properties, please note that each property is optional: - -```swift -let style = Skyflow.Style( - borderColor: UIColor, // optional - cornerRadius: CGFloat, // optional - padding: UIEdgeInsets, // optional - borderWidth: CGFloat, // optional - font: UIFont, // optional - textAlignment: NSTextAlignment, // optional - textColor: UIColor, // optional - boxShadow: CALayer, // optional - backgroundColor: UIColor, // optional - minWidth: CGFloat, // optional - maxWidth: CGFloat, // optional - minHeight: CGFloat, // optional - maxHeight: CGFloat, // optional - cursorColor: UIColor, // optional - width: CGFloat, // optional - height: CGFloat, // optional - placeholderColor: UIColor, // optional - cardIconAlignment: CardIconAlignment // optional, Skyflow.CardIconAlignment enum, default is .left -) -``` - -An example Skyflow.Styles object -```swift -let styles = Skyflow.Styles( - base: style, // optional - complete: style, // optional - empty: style, // optional - focus: style, // optional - invalid: style, // optional - requiredAsterisk: style // optional -) -``` - -The `labelStyles` and `errorTextStyles` fields accept the above mentioned `Skyflow.Styles` object which are applied to the `label` and `errorText` text views respectively. - -The states that are available for `labelStyles` are `base` and `focus`. - -`requiredAsterisk`: Styles applied for the Asterisk symbol in the label. Defaults to red. - -The state that is available for `errorTextStyles` is only the `base` state, it shows up when there is some error in the collect element. - -The parameters in `Skyflow.Style` object that are respected for `label` and `errorText` text views are -- padding -- font -- textColor -- textAlignment - -Other parameters in the `Skyflow.Style` object are ignored for `label` and `errorText` text views. - -Finally, the `type` parameter takes a Skyflow.ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 5 types: -- `INPUT_FIELD` -- `CARDHOLDER_NAME` -- `CARD_NUMBER` -- `EXPIRATION_DATE` -- `CVV` -- `PIN` -- `EXPIRATION_YEAR` -- `EXPIRATION_MONTH` - - -The `INPUT_FIELD` type is a custom UI element without any built-in validations. See the section on [validations](#validations) for more information on validations. - -Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. - -```swift -Skyflow.CollectElementOptions( - required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' - enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: String, // Format for the element - translation: [Character: String] // Indicates the allowed data type value for format. - enableCopy: Boolean, // Indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' - cardMetaData: [String: [Skyflow.CardType]] // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). -) -``` - -- `required`: Indicates whether the field is marked as required or not. Default is `false`. -- `enableCardIcon`: Indicates whether the icon is visible for the CARD_NUMBER element. Default is `true`. -- `format`: A string value that indicates the format pattern applicable to the element type. Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. - For INPUT_FIELD elements, - - the length of `format` determines the expected length of the user input. - - if `translation` isn't specified, the `format` value is considered a string literal. -- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. -- `enableCopy`: Indicates whether to enable the copy icon in collect elements to copy text to clipboard. -- `cardMetaData`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow-supported card types and determines which brands display in the card number element's card brand choice dropdown. `Skyflow.CardType` is an enum with all Skyflow-supported card schemes. - -```swift -import Skyflow - -const cardMetaData = [ "scheme": [Skyflow.CardType]] // Optional, array of skyflow supported card types. -``` - -#### Supported card types by Skyflow.CardType : -- `VISA` -- `MASTERCARD` -- `AMEX` -- `DINERS_CLUB` -- `DISCOVER` -- `JCB` -- `MAESTRO` -- `UNIONPAY` -- `HIPERCARD` -- `CARTES_BANCAIRES` - -Update cardMetaData: -```swift -cardNumberElement.update(updateOptions: CollectElementOptions(cardMetaData: ["scheme": [Skyflow.CardType.CARTES_BANCAIRES, Skyflow.CardType.MASTERCARD]])) -``` - -Accepted values by element type: - -| Element type | `format`and `translation` values | Examples | -| -------------| ------------------------------- | -------- | -| EXPIRATION_DATE |
  • `format`
    • `mm/yy`(default)
    • `mm/yyyy`
    • `yy/mm`
    • `yyyy/mm`
    |
    • 12/27
    • 12/2027
    • 27/12
    • 2027/12
| -| EXPIRATION_YEAR |
  • `format`
    • `yyyy` (default)
    • `yy`
|
  • 27
  • 2027
| -| CARD_NUMBER |
  • `format`
    • `XXXX XXXX XXXX XXXX` (default)
    • `XXXX-XXXX-XXXX-XXXX`
|
  • 1234 5678 9012 3456
  • 1234-5678-9012-3456
| -| INPUT_FIELD |
  • `format`: A string that matches the desired output, with placeholder characters of your choice.
  • `translation`: A dictionary of key/value pairs. Defaults to `[ "X": "[0-9]"]`
| With a `format` of `+91 XXXX-XX-XXXX` and a `translation` of `[ "X": "[0-9]"]`, user input of "1234121234" displays as "+91 1234-12-1234". | - -Collect Element Options examples for INPUT_FIELD -Example 1 -```swift -Skyflow.CollectElementOptions( - required: true, - enableCardIcon: true, - format: "+91 XXXX-XX-XXXX", - translation: [ "X": "[0-9]"] -) -``` -User input: "1234121234" - -Value displayed in INPUT_FIELD: "+91 1234-12-1234" - -Example 2 -```swift -Skyflow.CollectElementOptions( - required: true, - enableCardIcon: true, - format: "AY XX-XXX-XXXX", - translation: [ "X": "[0-9]", "Y": "[A-Z]"] -) -``` -User input: "B1234121234" - -Value displayed in INPUT_FIELD: "AB 12-341-2123" - -Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `Skyflow.CollectElementInput` object as defined above and the `options` parameter takes an `Skyflow.CollectElementOptions` object as described below: - -```swift -let collectElementInput = Skyflow.CollectElementInput( - table: String, // the table this data belongs to - column: String, // the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element - type: Skyflow.ElementType, // Skyflow.ElementType enum -) - -let collectElementOptions = Skyflow.CollectElementOptions( - required: false, // indicates whether the field is marked as required. Defaults to 'false', - enableCardIcon: true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: "mm/yy" // format for the element, - enableCopy: true, // indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' -) - -let element = container?.create(input: collectElementInput, options: collectElementOptions) -``` - - - -### Step 3: Mount Elements to the Screen - -To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add it as a subview programmatically. - -```swift -let stackView = UIStackView() -stackView.addArrangedSubview(element) -``` - -The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state - -``` swift -func clearFieldsOnSubmit(_ elements: [TextField]) { - // resets all elements in the array - for element in elements { - element.unmount() - } -} -``` - -#### Step 4 : Collect data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: -```swift -// Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] -// Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() -container?.collect(callback: insertCallback, options: options) - -``` - -### Collect data with Skyflow Elements - -#### Collect call example: -```swift - -//Initialize skyflow configuration. -let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) - -//Initialize skyflow client. -let skyflowClient = Skyflow.initialize(config) - -//Create a CollectContainer. -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) - -//Create Skyflow.Styles with individual Skyflow.Style variants. -let baseStyle = Skyflow.Style(borderColor: UIColor.blue) -let baseTextStyle = Skyflow.Style(textColor: UIColor.black) -let completeStyle = Skyflow.Style(borderColor: UIColor.green) -let focusTextStyle = Skyflow.Style(textColor: UIColor.red) -let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) -let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) -let errorTextStyles = Skyflow.Styles(base: baseTextStyle) - -let iconStyles = Skyflow.Styles(base: Style(cardIconAlignment: .left)) - -// Create a CollectElementInput. -let input = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - iconStyles: iconStyles, - label: "card number", - placeholder: "card number", - type: Skyflow.ElementType.CARD_NUMBER -) - -// Create an option to require the element. -let requiredOption = Skyflow.CollectElementOptions(required: true, enableCopy: true) - -// Create a Collect Element from the Collect Container. -let skyflowElement = container?.create(input: input, options: requiredOption) - -// Can interact with this object as a normal UIView Object and add to View - -// Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] - - //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() - -// Call collect method on CollectContainer. -container?.collect(callback: insertCallback, options: collectOptions) -``` - -#### Skyflow returns tokens for the record you just inserted: -``` -{ - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" - } - }, { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - } - }] -} -``` - -## Using Skyflow Elements to update data -You can update the data in a vault with Skyflow Elements. Use the following steps to securely update data. - -### Step 1: Create a container - -First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below - -```swift -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) -``` - -### Step 2: Create a collect Element -To create a collect Element, we must first construct a Skyflow.CollectElementInput object defined as shown below: - -```swift -let collectElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element - type: Skyflow.ElementType, // Skyflow.ElementType enum - skyflowID: String, // The skyflow_id of the record to be updated. -) -``` - -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. - -**Note**: -- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) -- `table` and `column` are optional only if the element is being used in invokeConnection() - -Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. - -```swift -Skyflow.CollectElementOptions( - required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' - enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: String, // Format for the element - translation: [Character: String] // Indicates the allowed data type value for format. - enableCopy: Boolean, // Indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' - cardMetaData: [String: [Skyflow.CardType]] // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). -) -``` - -### Step 3: Mount Elements to the Screen - -To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add it as a subview programmatically. - -```swift -let stackView = UIStackView() -stackView.addArrangedSubview(element) -``` - -The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state - -``` swift -func clearFieldsOnSubmit(_ elements: [TextField]) { - // resets all elements in the array - for element in elements { - element.unmount() - } -} -``` - -### Step 4 : Update data from Elements -When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. -- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. - -```swift -// Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowID": "value"]]] -// Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() -container?.collect(callback: insertCallback, options: options) -``` - -**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. - -### End to end example of updating data with Skyflow Elements -```swift - -//Initialize skyflow configuration. -let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) - -//Initialize skyflow client. -let skyflowClient = Skyflow.initialize(config) - -//Create a CollectContainer. -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) - -//Create Skyflow.Styles with individual Skyflow.Style variants. -let baseStyle = Skyflow.Style(borderColor: UIColor.blue) -let baseTextStyle = Skyflow.Style(textColor: UIColor.black) -let completeStyle = Skyflow.Style(borderColor: UIColor.green) -let focusTextStyle = Skyflow.Style(textColor: UIColor.red) -let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) -let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) -let errorTextStyles = Skyflow.Styles(base: baseTextStyle) - -let iconStyles = Skyflow.Styles(base: Style(cardIconAlignment: .left)) - -// Create a CollectElementInput. -let input = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - iconStyles: iconStyles, - label: "card number", - placeholder: "card number", - type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" -) - -// Create an option to require the element. -let requiredOption = Skyflow.CollectElementOptions(required: true, enableCopy: true) - -// Create a Collect Element from the Collect Container. -let skyflowElement = container?.create(input: input, options: requiredOption) - -// Can interact with this object as a normal UIView Object and add to View - -// Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882"]] - - //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() - -// Call collect method on CollectContainer. -container?.collect(callback: insertCallback, options: collectOptions) -``` - - -#### Skyflow returns tokens for the record you just updated: -```json -{ - "records": [ - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } - }, - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051" - } - } - ] -} -``` - -## Validations - -Skyflow-iOS provides two types of validations on Collect Elements - -#### 1. Default Validations: -Every Collect Element except of type `INPUT_FIELD` has a set of default validations listed below: -- `CARD_NUMBER`: Card number validation with checkSum algorithm(Luhn algorithm), available card lengths for defined card types -- `CARD_HOLDER_NAME`: Name should be 2 or more symbols, valid characters should match pattern - `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$` -- `CVV`: Card CVV can have 3-4 digits -- `EXPIRATION_DATE`: Any date starting from current month. By default valid expiration date should be in short year format - `MM/YY` -- `PIN`: Can have 4-12 digits - -#### 2. Custom Validations: -Custom validations can be added to any element which will be checked after the default validations have passed. The following Custom validation rules are currently supported: -- `RegexMatchRule`: You can use this rule to specify any Regular Expression to be matched with the text field value -- `LengthMatchRule`: You can use this rule to set the minimum and maximum permissible length of the textfield value -- `ElementValueMatchRule`: You can use this rule to match the value of one element with another - -The Sample code below illustrates the usage of custom validations: - -```swift -/* - Reset Password - A simple example that illustrates custom validations. - The below code shows two input fields with custom validations, - one to enter a password and the second to confirm the same password. - */ - -var myRuleset = ValidationSet() -let strongPasswordRule = RegexMatchRule( - regex: "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]*$", - error: "At least one letter and one number" -) // This rule enforces a strong password - -let lengthRule = LengthMatchRule( - minLength: 8, - maxLength: 16, - error: "Must be between 8 and 16 digits" -) // this rule allows input length between 8 and 16 characters - -// for the Password element -myRuleset.add(rule: strongPasswordRule) -myRuleset.add(rule: lengthRule) - -let collectElementOptions = CollectElementOptions(required: true) - -let passwordInput = CollectElementInput( - inputStyles: styles, - label: "password", - placeholder: "********", - type: .INPUT_FIELD, - validations: myRuleset -) -let password = container?.create(input: passwordInput, options: collectElementOptions) - -// For confirm password element - shows error when the passwords don't match -let elementValueMatchRule = ElementValueMatchRule(element: password!, error: "passwords don't match") -let confirmPasswordInput = CollectElementInput( - inputStyles: styles, - label: "Confirm password", - placeholder: "********", - type: .INPUT_FIELD, - validations: ValidationSet(rules: [strongPasswordRule, lengthRule, elementValueMatchRule]) -) -let confirmPassword = container?.create(input: confirmPasswordInput, options: collectElementOptions) - -// mount elements on screen - errors will be shown if any of the validaitons fail -stackView.addArrangedSubview(password!) -stackView.addArrangedSubview(confirmPassword!) -``` - -## Event Listener on Collect Elements -Helps to communicate with skyflow elements by listening to an event - -```swift -element!.on(eventName: Skyflow.EventName) { _ in - // handle function -} -``` - -There are 4 events in `Skyflow.EventName` -- `CHANGE` - Change event is triggered when the Element's value changes. -- `READY` - Ready event is triggered when the Element is fully rendered -- `FOCUS` - Focus event is triggered when the Element gains focus -- `BLUR` - Blur event is triggered when the Element loses focus. -The handler ```(state: [String: Any]) -> Void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. - -```swift -let state = [ - "elementType": Skyflow.ElementType, - "isEmpty": Bool , - "isFocused": Bool, - "isValid": Bool, - "value": String, - "selectedCardScheme": String // only for CARD_NUMBER element type -] -``` -`Note:` -- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. -- `selectedCardScheme` is only populated for the `CARD_NUMBER` element state when a user chooses a card brand. By default, `selectedCardScheme` is an empty string. - -##### Sample code snippet for using listeners -```swift -// create skyflow client with loglevel:"DEBUG" -let config = Skyflow.Configuration( - vaultID: VAULT_ID, - vaultURL: VAULT_URL, - tokenProvider: demoTokenProvider, - options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) -) - -let skyflowClient = Skyflow.initialize(config) - -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) - -// Create a CollectElementInput -let cardNumberInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - type: Skyflow.ElementType.CARD_NUMBER, - ) -let cardHolderNameInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardHolderName", - type: Skyflow.ElementType.CARDHOLDER_NAME, - ) - -let cardNumber = container?.create(input: cardNumberInput) -let cardHolderName = container?.create(input: cardHolderNameInput) - - -// subscribing to CHANGE event, which gets triggered when element changes -cardNumber.on(eventName: Skyflow.EventName.CHANGE) { state in - // Your implementation when Change event occurs - print(state) -} - -cardHolderName.on(eventName: Skyflow.EventName.CHANGE) { state in - // Your implementation when Change event occurs - print(state) -} - -``` -##### Sample Element state object when `env` is `DEV` -```swift -[ - "elementType": Skyflow.ElementType.CARD_NUMBER, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "4111111111111111" -] -[ - "elementType": Skyflow.ElementType.CARDHOLDER_NAME, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "John" -] - -``` -##### Sample Element state object when `env` is `PROD` -```swift -[ - "elementType": Skyflow.ElementType.CARD_NUMBER, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "41111111XXXXXXXX" -] -[ - "elementType": Skyflow.ElementType.CARDHOLDER_NAME, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "" -] -``` -## UI Error for Collect Elements - -Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. - -`setError(error: String)` method is used to set the error text for the element, when this method is trigerred, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is trigerred on the same element. - -`resetError()` method is used to clear the custom error message that is set using `setError`. - -##### Sample code snippet for setError and resetError - -```swift -// Create skyflow client with loglevel:"DEBUG" -let config = Skyflow.Configuration( - vaultID: VAULT_ID, - vaultURL: VAULT_URL, - tokenProvider: demoTokenProvider, - options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) -) - -let skyflowClient = Skyflow.initialize(config) - -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) - -// Create a CollectElementInput -let cardNumberInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - type: Skyflow.ElementType.CARD_NUMBER -) - -let cardNumber = container.create(input: cardNumberInput) - -// Set custom error -cardNumber.setError("custom error") - -// Reset custom error -cardNumber.resetError() -``` - -## Set and Clear value for Collect Elements (DEV ENV ONLY) - -`setValue(value: String)` method is used to set the value of the element. This method will override any previous value present in the element. - -`clearValue()` method is used to reset the value of the element. - -`Note:` This methods are only available in DEV env for testing/developmental purposes and MUST NOT be used in PROD env. - -##### Sample code snippet for setValue and clearValue - -```swift -// Create skyflow client with env DEV -let config = Skyflow.Configuration( - vaultID: VAULT_ID, - vaultURL: VAULT_URL, - tokenProvider: demoTokenProvider, - options: Skyflow.Options(env: Skyflow.Env.DEV) -) - -let skyflowClient = Skyflow.initialize(config) - -let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) - -// Create a CollectElementInput -let cardNumberInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - type: Skyflow.ElementType.CARD_NUMBER -) - -let cardNumber = container.create(input: cardNumberInput) - -// Set a value programatically -cardNumber.setValue("4111111111111111") - -// Clear the value -cardNumber.clearValue() -``` - -# Securely collecting data client-side using Composable Elements -- [Using Skyflow Composable Elements to collect data](#using-skyflow-composable-elements-to-collect-data) -- [Using Skyflow Composable Elements to update data](#using-skyflow-composable-elements-to-update-data) - -## Using Skyflow Composable Elements to collect data -Composable Elements combine multiple Skyflow Elements in a single row. The following steps create a composable element and securely collect data through it. - -### Step 1: Create a composable container - -First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below - -```swift -let container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, options: ContainerOptions) -``` - -The container requires an options object that contains the following keys: - -- `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. - - For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. - - `Note`: The sum of values in the layout array should be equal to the number of elements created - -- `styles`: styles to apply to each composable row. - -- `errorTextStyles`: styles to apply if an error is encountered. - -```swift -var containerOptions = ContainerOptions( - layout: [1,1,2], // required - styles: Skyflow.Styles, // optional - errorTextStyles: Skyflow.Styles //optional - ) -``` -### Step 2: Create Composable Elements -Composable Elements use the following schema: - -```swift -let composableElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element - type: Skyflow.ElementType, // Skyflow.ElementType enum -) -``` -The `table` and `column` fields indicate which table and column in the vault the Element correspond to. -**Note**: -- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) - -The `inputStyles` parameter accepts a Skyflow.Styles object which consists of multiple `Skyflow.Styles` objects which should be applied to the form element in the following states: - -- `base`: all other variants inherit from these styles -- `complete`: applied when the Element has valid input -- `empty`: applied when the Element has no input -- `focus`: applied when the Element has focus -- `invalid`: applied when the Element has invalid input - -Each Style object accepts the following properties, please note that each property is optional: - -```swift -let style = Skyflow.Style( - borderColor: UIColor, // optional - cornerRadius: CGFloat, // optional - padding: UIEdgeInsets, // optional - borderWidth: CGFloat, // optional - font: UIFont, // optional - textAlignment: NSTextAlignment, // optional - textColor: UIColor // optional -) -``` -An example Skyflow.Styles object -```swift -let styles = Skyflow.Styles( - base: style, // optional - complete: style, // optional - empty: style, // optional - focus: style, // optional - invalid: style // optional -) -``` - -The `labelStyles` and `errorTextStyles` fields accept the above mentioned `Skyflow.Styles` object which are applied to the `label` and `errorText` text views respectively. - -The states that are available for `labelStyles` are `base` and `focus`. - -The state that is available for `errorTextStyles` is only the `base` state, it shows up when there is some error in the composable element. - -The parameters in `Skyflow.Style` object that are respected for `label` and `errorText` text views are -- padding -- font -- textColor -- textAlignment - -Other parameters in the `Skyflow.Style` object are ignored for `label` and `errorText` text views. - -Finally, the `type` parameter takes a Skyflow.ElementType. Each type applies the appropriate regex and validations to the form element. - -The iOS SDK supports the following composable elements: - -- `INPUT_FIELD` -- `CARDHOLDER_NAME` -- `CARD_NUMBER` -- `EXPIRATION_DATE` -- `CVV` -- `PIN` -- `EXPIRATION_YEAR` -- `EXPIRATION_MONTH` - -**Note**: Only when the entered value in the below composable elements is valid, the focus shifts automatically. The element types are: - -- `CARD_NUMBER` -- `EXPIRATION_DATE` -- `EXPIRATION_MONTH` -- `EXPIRATION_YEAR` - -The `INPUT_FIELD` type is a custom UI element without any built-in validations. See the section on [validations](#validations) for more information on validations. - -Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. - -```swift -Skyflow.CollectElementOptions( - required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' - enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: String, // Format for the element - translation: [Character: String] // Indicates the allowed data type value for format. -) -``` -- `required`: Indicates whether the field is marked as required or not. Default is `false`. -- `enableCardIcon`: Indicates whether the icon is visible for the CARD_NUMBER element. Default is `true`. -- `format`: A string value that indicates the format pattern applicable to the element type. Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. - For INPUT_FIELD elements, - - the length of `format` determines the expected length of the user input. - - if `translation` isn't specified, the `format` value is considered a string literal. -- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. - -The accepted EXPIRATION_DATE values are - -- `mm/yy` (default) -- `mm/yyyy` -- `yy/mm` -- `yyyy/mm` - -The accepted EXPIRATION_YEAR values are - -- `yy` (default) -- `yyyy` - -Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `Skyflow.CollectElementInput` object as defined above and the `options` parameter takes an `Skyflow.CollectElementOptions` object as described below: - -```swift -let composableElementInput = Skyflow.CollectElementInput( - table: String, // the table this data belongs to - column: String, // the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element - type: Skyflow.ElementType, // Skyflow.ElementType enum -) - -let collectElementOptions = Skyflow.CollectElementOptions( - required: false, // indicates whether the field is marked as required. Defaults to 'false', - enableCardIcon: true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: "mm/yy" // Format for the element -) - -let element = container?.create(input: composableElementInput, options: collectElementOptions) -``` -### Step 3: Mount Elements to the Screen -To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add composable elements view using `container?.getComposableView()`, it as a subview programmatically. - -```swift -let stackView = UIStackView() -do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} -``` - -The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state - -``` swift -func clearFieldsOnSubmit(_ elements: [TextField]) { - // resets all elements in the array - for element in elements { - element.unmount() - } -} -``` -### Step 4: Collect data from elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. -The options parameter takes a `Skyflow.CollectOptions` object as shown below: +This repository publishes two SDKs from a shared codebase. Pick the one that matches your vault type: -- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. +| SDK | Vault type | Install (CocoaPods / SPM product) | Import | Documentation | +|-----|------------|-----------------------------------|--------|---------------| +| **Skyflow** | PDB vault (v1 API) | `pod 'Skyflow'` / `Skyflow` | `import Skyflow` | [Skyflow/README.md](Skyflow/README.md) | +| **SkyflowFlowVault** | Flow vault (v2 API, beta) | `pod 'SkyflowFlowVault'` / `SkyflowFlowVault` | `import SkyflowFlowVault` | [SkyflowFlowVault/README.md](SkyflowFlowVault/README.md) | -```swift -// Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] -// Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) +Not sure which one you need? If you are an existing Skyflow iOS customer, stay on **Skyflow** — it is fully backward compatible. **SkyflowFlowVault** is for Flow vaults and includes the migration guide for moving from PDB to FlowDB. -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() -container?.collect(callback: insertCallback, options: options) -``` -End to end example of collecting data with Composable Elements +## Repository layout -```swift - -//Initialize skyflow configuration. -let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) - -//Initialize skyflow client. -let skyflowClient = Skyflow.initialize(config) - -let containerOptions = ContainerOptions(layout: [1,2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) -//Create a Composable Container. -let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) - -//Create Skyflow.Styles with individual Skyflow.Style variants. -let baseStyle = Skyflow.Style(borderColor: UIColor.blue) -let baseTextStyle = Skyflow.Style(textColor: UIColor.black) -let completeStyle = Skyflow.Style(borderColor: UIColor.green) -let focusTextStyle = Skyflow.Style(textColor: UIColor.red) -let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) -let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) -let errorTextStyles = Skyflow.Styles(base: baseTextStyle) - -// Create Composable Elements. - -let cardHolderNameElementInput = Skyflow.CollectElementInput( - table: "cards", - column: "first_name", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "first name", - placeholder: "first name", - type: Skyflow.ElementType.CARDHOLDER_NAME -) -// Create an option to require the element. -let requiredOption = Skyflow.CollectElementOptions(required: true) - -// Create a Composable Element from the Composable Container. -let cardHolderNameElement = container?.create(input: cardHolderNameElementInput, options: requiredOption) - - -let cardNumberElementInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "card number", - placeholder: "card number", - type: Skyflow.ElementType.CARD_NUMBER -) - -let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) - -let cvvElementInput = Skyflow.CollectElementInput( - table: "cards", - column: "cvv", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "cvv", - placeholder: "cvv", - type: Skyflow.ElementType.CVV -) - -let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) - -// Can interact with this object as a normal UIView Object and add to View -let stackView = UIStackView() -do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} - -// Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] - - //Upsert options -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() - -// Call collect method on CollectContainer. -container?.collect(callback: insertCallback, options: collectOptions) -``` -### Sample Response: - -``` -{ - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" - } - }, { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - } - }] -} -``` -[For information on validations, see validations.](#validations) - - - -## Set an event listener on Composable Elements: -You can communicate with Skyflow Elements by listening to element events: - -```swift -element!.on(eventName: Skyflow.EventName) { _ in - // handle function -} -``` - -The SDK supports four events: - -- CHANGE: Triggered when the Element's value changes. -- READY: Triggered when the Element is fully rendered. -- FOCUS: Triggered when the Element gains focus. -- BLUR: Triggered when the Element loses focus. - -The handler ```(state: [String: Any]) -> Void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. - -```swift -let state = [ - "elementType": Skyflow.ElementType, - "isEmpty": Bool , - "isFocused": Bool, - "isValid": Bool, - "value": String -] -``` - -`Note`: -values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. - -#### Example Usage of Event Listener on Composable Elements - -```swift -// create skyflow client with loglevel:"DEBUG" -let config = Skyflow.Configuration( - vaultID: VAULT_ID, - vaultURL: VAULT_URL, - tokenProvider: demoTokenProvider, - options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) -) - -let skyflowClient = Skyflow.initialize(config) - -let containerOptions = ContainerOptions(layout: [2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) -//Create a Composable Container. -let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) - -// Create a CollectElementInput -let cardNumberInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - type: Skyflow.ElementType.CARD_NUMBER, - ) -let cardHolderNameInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardHolderName", - type: Skyflow.ElementType.CARDHOLDER_NAME, - ) - -let cardNumber = container?.create(input: cardNumberInput) -let cardHolderName = container?.create(input: cardHolderNameInput) - -let stackView = UIStackView() -do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} -// subscribing to CHANGE event, which gets triggered when element changes -cardNumber.on(eventName: Skyflow.EventName.CHANGE) { state in - // Your implementation when Change event occurs - print(state) -} - -cardHolderName.on(eventName: Skyflow.EventName.CHANGE) { state in - // Your implementation when Change event occurs - print(state) -} -``` - -##### Sample Element state object when `env` is `DEV` -```swift -[ - "elementType": Skyflow.ElementType.CARD_NUMBER, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "4111111111111111" -] -[ - "elementType": Skyflow.ElementType.CARDHOLDER_NAME, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "John" -] - -``` -##### Sample Element state object when `env` is `PROD` -```swift -[ - "elementType": Skyflow.ElementType.CARD_NUMBER, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "41111111XXXXXXXX" -] -[ - "elementType": Skyflow.ElementType.CARDHOLDER_NAME, - "isEmpty": false, - "isFocused": true, - "isValid": true, - "value": "" -] -``` -### Update composable elements -You can update composable element properties with the `update` interface. - -The `update` interface takes the below object: -```swift -let updateElement = Skyflow.CollectElementInput( - table: String, // optional the table this data belongs to - column: String, // optional the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element -) -``` -Only include the properties that you want to update for the specified composable element. - -Properties your provided when you created the element remain the same until you explicitly update them. - -`Note`: You can't update the type property of an element. - -### End to end example -```swift -// create skyflow client with loglevel:"DEBUG" -let config = Skyflow.Configuration( - vaultID: VAULT_ID, - vaultURL: VAULT_URL, - tokenProvider: demoTokenProvider, - options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) -) - -let skyflowClient = Skyflow.initialize(config) - -let containerOptions = ContainerOptions(layout: [2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) -//Create a Composable Container. -let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) - -// Create a CollectElementInput -let cardNumberInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - type: Skyflow.ElementType.CARD_NUMBER, - ) -let cardHolderNameInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardHolderName", - type: Skyflow.ElementType.CARDHOLDER_NAME, - ) - -let cardNumber = container?.create(input: cardNumberInput) -let cardHolderName = container?.create(input: cardHolderNameInput) - -let stackView = UIStackView() -do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} -// Update table, column, inputStyles properties on cardNumber. -cardNumber.update(update: CollectElementInput( - table: "cards", - column: "cardHolderName", - inputStyles: Skyflow.Styles(base: Style(borderColor: UIColor.red)) -)) -let lengthRule = LengthMatchRule( - minLength: 5, - maxLength: 16, - error: "Must be between 5 and 16 digits") - -// Update validations and placeholder property on cardHolderName. -cardHolderName.update(update: CollectElementInput( - placeholder: "cardHolderName", - validations: ValidationSet(rules: [lengthRule])) -)) -``` - -### Set an event listener on a composable container - -Currently, the SDK supports one event: - - - `SUBMIT`: Triggered when the Enter key is pressed in any container element. - -The handler function(void) -> void is a callback function you provide that's called when the `SUBMIT` event fires. - -#### Example - -```swift -let containerOptions = ContainerOptions(layout: [1], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) -//Create a Composable Container. -let composableContainer = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) - -// Creating the element. -let cardNumberInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - type: Skyflow.ElementType.CARD_NUMBER, - ) -let cardNumber = composableContainer?.create(input: cardNumberInput) - -// get composable view and mount it -let stackView = UIStackView() -do { - let composableView = try composableContainer?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} -// Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. -composableContainer?.on(eventName: .SUBMIT){ - // Your implementation when the SUBMIT(enter) event occurs. - print('Submit Event Listener is being Triggered.') -} - -``` - -## Using Skyflow Composable Elements to update data -Composable Elements combine multiple Skyflow Elements in a single row. The following steps create a composable element and securely update data through it. - -### Step 1: Create a composable container -First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below - -```swift -var containerOptions = ContainerOptions( - layout: [1,1,2], // required - styles: Skyflow.Styles, // optional - errorTextStyles: Skyflow.Styles //optional - ) -let container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, options: ContainerOptions) -``` - -### Step 2: Create Composable Elements -Composable Elements use the following schema: - -```swift -let composableElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be inserted - inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element - labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element - errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element - label: String, // optional label for the form element - placeholder: String, // optional placeholder for the form element - altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element - validations: ValidationSet, // optional set of validations for the input element - type: Skyflow.ElementType, // Skyflow.ElementType enum - skyflowID: String, // The skyflow_id of the record to be updated. -) -``` -The `table` and `column` fields indicate which table and column in the vault the Element correspond to. -**Note**: -- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) - -Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. - -```swift -Skyflow.CollectElementOptions( - required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' - enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: String, // Format for the element - translation: [Character: String] // Indicates the allowed data type value for format. -) -``` - - -Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `Skyflow.CollectElementInput` object as defined above and the `options` parameter takes an `Skyflow.CollectElementOptions` object as described below: - - ```swift -let element = container?.create(input: composableElementInput, options: collectElementOptions) -``` -### Step 3: Mount Elements to the Screen - -To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add composable elements view using `container?.getComposableView()`, it as a subview programmatically. - -```swift -let stackView = UIStackView() -do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} -``` - -The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state - -``` swift -func clearFieldsOnSubmit(_ elements: [TextField]) { - // resets all elements in the array - for element in elements { - element.unmount() - } -} -``` -### Step 4: Update data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. -The options parameter takes a `Skyflow.CollectOptions` object as shown below: - -- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. - -```swift -// Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] -// Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() -container?.collect(callback: insertCallback, options: options) -``` -End to end example of collecting data with Composable Elements - -```swift - -//Initialize skyflow configuration. -let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) - -//Initialize skyflow client. -let skyflowClient = Skyflow.initialize(config) - -let containerOptions = ContainerOptions(layout: [1,2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) -//Create a Composable Container. -let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) - -//Create Skyflow.Styles with individual Skyflow.Style variants. -let baseStyle = Skyflow.Style(borderColor: UIColor.blue) -let baseTextStyle = Skyflow.Style(textColor: UIColor.black) -let completeStyle = Skyflow.Style(borderColor: UIColor.green) -let focusTextStyle = Skyflow.Style(textColor: UIColor.red) -let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) -let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) -let errorTextStyles = Skyflow.Styles(base: baseTextStyle) - -// Create Composable Elements. - -let cardHolderNameElementInput = Skyflow.CollectElementInput( - table: "cards", - column: "first_name", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "first name", - placeholder: "first name", - type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" -) -// Create an option to require the element. -let requiredOption = Skyflow.CollectElementOptions(required: true) - -// Create a Composable Element from the Composable Container. -let cardHolderNameElement = container?.create(input: cardHolderNameElementInput, options: requiredOption) - - -let cardNumberElementInput = Skyflow.CollectElementInput( - table: "cards", - column: "cardNumber", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "card number", - placeholder: "card number", - type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" -) - -let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) - -let cvvElementInput = Skyflow.CollectElementInput( - table: "cards", - column: "cvv", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "cvv", - placeholder: "cvv", - type: Skyflow.ElementType.CVV, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" -) - -let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) - -// Can interact with this object as a normal UIView Object and add to View -let stackView = UIStackView() -do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) -} catch { - print(error) -} - -// Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf"]] - - //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() - -// Call collect method on CollectContainer. -container?.collect(callback: insertCallback, options: collectOptions) -``` -### Sample Success Response: -```json -{ - "records": [ - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } - }, - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ] -} -``` -### Sample Partial Error Response: -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ], - "errors": [ - { - "error": { - "code": 400, - "message": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs. - request-id: cb397-8521-42c2-870c-92dbeec" - } - } - ] -} -``` - -# Securely revealing data client-side -- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) -- [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) -- [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) -- [**Set token for Reveal Elements**](#set-token-for-reveal-elements) -- [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) - -## Retrieving data from the vault -For non-PCI use-cases, retrieving data from the vault and revealing it in the mobile can be done either using the SkyflowID's or tokens as described below - -- ### Using Skyflow tokens - To retrieve record data using tokens, use the `detokenize(records)` method. The records parameter takes a Dictionary object that contains tokens for `record` values to fetch - ```swift - [ - "records": [ - [ - "token": String, - "redaction": Skyflow.RedactionType // Optional. Redaction to apply for retrieved data. - ] - ] - ] - ``` - Note: `redaction` defaults to [RedactionType.PLAIN_TEXT](#redaction-types). - -The following example code makes a detokenize call to reveal the masked value of a token: - -```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let records = [ - "records": [ - [ - "token": "45012507-f72b-4f5c-9bf9-86b133bae719", - ], - [ - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "redaction": Skyflow.RedactionType.MASKED - ] - ] - ] as [String: Any] - - skyflowClient.detokenize(records: records, callback: getCallback) - ``` - The sample response: - ```json - { - "records": [ - { - "token": "131e70dc-6f76-4319-bdd3-96281e051051", - "value": "1990-01-01" - }, - { - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "value": "xxxxxxer", - } - ] - } - ``` - -- ### Using Skyflow ID's or Unique Column Values - For retrieving data from the vault, use the `get(records: JSONObject, options: GetOptions? = GetOptions(), callback: Skyflow.Callback)` method. - The `records` parameter takes a Dictionary object that contains `records` to be fetched as shown below. Each object inside array should contain: - - - Either an array of Skyflow IDs to fetch - - Or a column name and an array of column values - - The second parameter, `options`, is a `GetOptions` object that retrieves tokens of Skyflow IDs. - - Notes: - - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. - - GetOptions parameter is applicable only for retrieving tokens using Skyflow ID. - - You can't pass GetOptions along with the redaction type. - - tokens defaults to false. - - ```swift - [ - "records": [ - [ - "ids": ArrayList(), // Array of SkyflowID's of the records to be fetched - "table": String, // Name of table holding the above skyflow_id's - "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data - ], - [ - "columnValues": ArrayList(), // Array of column values of the records to be fetched - "table": String, // Name of table holding the above skyflow_id's - "columnName": String, // A unique column name - "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data - ], - ] - ] - ``` - ### Redaction Types - There are 4 accepted values in Skyflow.RedactionTypes: - - `PLAIN_TEXT` - - `MASKED` - - `REDACTED` - - `DEFAULT` - - An example of get call to fetch records: - ```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9"] - let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let columnValues = ["12345"] - let columnRecord = ["columnValues": columnValues,"columnName": "card_pin", "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record,columnRecord, badRecord]] - - skyflowClient.get(records: records, callback: getCallback) - ``` - - The sample response: - ```json - { - "records": [{ - "fields": { - "card_number": "4111111111111111", - "cvv": "127", - "expiry_date": "11/35", - "fullname": "myname", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - },{ - "fields": { - "card_number": "4111111111111111", - "cvv": "345", - "card_pin":"12345" - "expiry_date": "12/29", - "fullname": "joey", - "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" - }, - "table": "cards" - } - ], - "errors": [ { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - }] - } - ``` - - An example of get call to fetch tokens: - - ```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9", "da26de53-95d5-4bdb-99db-8d8c66a35ff9"] - let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, badRecord]] - - skyflowClient.get(records: records, options: GetOptions(tokens: true), callback: getCallback) - ``` - The sample Response: - ```json - { - "records": [ - { - "fields": { - "card_number": "9802-3257-3113-0294", - "expiry_date": "45012507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "0294-3213-3157-9802", - "expiry_date": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "45012507-f72b-4f5c-9bf9-86b133bae719", - "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" - }, - "table": "cards" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - } - ] -} - ``` - - -## Using Skyflow Elements to reveal data -Skyflow Elements can be used to securely reveal data in an application without exposing your front end to the sensitive data. This is great for use-cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. -### Step 1: Create a container -To start, create a container using the `skyflowClient.container(Skyflow.ContainerType.REVEAL)` method as shown below. -```swift - let container = skyflowClient.container(type: Skyflow.ContainerType.REVEAL) -``` - -### Step 2: Create a reveal Element -To create a reveal Element, we must first construct a Skyflow.RevealElementInput object defined as shown below: - -```swift -let revealElementInput = Skyflow.RevealElementInput( - token: String, // optional, token of the data being revealed - inputStyles: Skyflow.Styles(), // optional, styles to be applied to the element - labelStyles: Skyflow.Styles(), // optional, styles to be applied to the label of the reveal element - errorTextStyles: Skyflow.Styles(), // optional styles that will be applied to the errorText of the reveal element - label: "cardNumber", // optional, label for the element, - altText: "XXXX XXXX XXXX XXXX", // optional, string that is shown before reveal, will show token if it is not provided - redaction: Skyflow.RedactionType, // optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED -) -``` -`Note`: -- `token` is optional only if it is being used in invokeConnection() -- `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). - - -The `inputStyles` parameter accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but the only state available for a reveal element is the base state. - -The `labelStyles` and `errorTextStyles` fields accept the above mentioned `Skyflow.Styles` object as described in the [previous section](#step-2-create-a-collect-element), the only state available for a reveal element is the base state. - -The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but only a single variant is available i.e. base. - -An example of a inputStyles object: - -```swift -let inputStyles = Skyflow.Styles(base: Skyflow.Style(borderColor: UIColor.green)) -``` - -An example of a labelStyles object: - -```swift -let labelStyles = Skyflow.Styles(base: Skyflow.Style(font: UIFont (name: "GILLSANSCE-ROMAN", size: 12)))) -``` - -An example of a errorTextStyles object: - -```swift -let labelStyles = Skyflow.Styles(base: Skyflow.Style(textColor: UIColor.red)) -``` -Along with `RevealElementInput`, you can define other options in the `RevealElementOptions` object as described below: -```swift -Skyflow.RevealElementOptions( - format: String, // Format for the element. - translation: [Character: String] // Indicates the allowed data type value for format - enableCopy: Boolean, // Indicates whether to enable the copy icon in reveal elements to copy text to clipboard. Defaults to 'false' -) -``` -- `format`: A string value that indicates how the element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified, the `format` value is considered a string literal. -- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `[ "X": "[0-9]"]`. -- `enableCopy`: Indicates whether to enable the copy icon in reveal elements to copy text to clipboard. - -Reveal Element Options examples: - -Example 1: -```swift -Skyflow.RevealElementOptions( - format: "(XXX) XXX-XXXX", - translation: [ "X": "[0-9]"] -) -``` -Value from vault: "1234121234" - -Value displayed in element: "(123) 412-1234" - -Example 2: -```swift -Skyflow.RevealElementOptions( - format: "XXXX-XXXXXX-XXXXX", - translation: [ "X": "[0-9]"] -) -``` -Value from vault: "374200000000004" - -Value displayed in element: "3742-000000-00004" - -Once you've defined a `Skyflow.RevealElementInput` object and `Skyflow.RevealElementOptions`, you can use the `create()` method of the container to create the Element as shown below: - -```swift -let element = container.create(input: revealElementInput, options: Skyflow.RevealElementOptions(format: "XXXX-XXXXXX-XXXXX", -translation: ["X": "[0-9]"] -)) -``` - -### Step 3: Mount Elements to the Screen - -Elements used for revealing data are mounted to the screen the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-screen). - -### Step 4: Reveal data -When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: -```swift -let revealCallback = RevealCallback() // Custom callback - implementation of Skyflow.Callback -container.reveal(callback: revealCallback) -``` - -### UI Error for Reveal Elements - -Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. - -`setError(error: String)` method is used to set the error text for the element, when this method is trigerred, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is trigerred on the same element. - -`resetError()` method is used to clear the custom error message that is set using `setError`. - -### Set token for Reveal Elements - -The `setToken(value: String)` method can be used to set the token of the Reveal Element. If no altText is set, the set token will be displayed on the UI as well. If altText is set, then there will be no change in the UI but the token of the element will be internally updated. - -### Set and Clear altText for Reveal Elements - -The `setAltText(value: String)` method can be used to set the altText of the Reveal Element. This will cause the altText to be displayed in the UI regardless of whether the token or value is currently being displayed. - -`clearAltText()` method can be used to clear the altText, this will cause the element to display the token or actual value of the element. If the element has no token, the element will be empty. - -### Reveal data with Skyflow Elements -#### End-to-end example of revealing data with Skyflow Elements -```swift -// Initialize skyflow configuration -let config = Skyflow.Configuration(vaultID: , vaultURL: , tokenProvider: demoTokenProvider) - -// Initialize skyflow client -let skyflowClient = Skyflow.initialize(config) - -// Create a Reveal Container -let container = skyflowClient.container(type: Skyflow.ContainerType.REVEAL) - -// Create Skyflow.Styles with individual Skyflow.Style variants -let baseStyle = Skyflow.Style(borderColor: UIColor.blue) -let baseTextStyle = Skyflow.Style(textColor: UIColor.BLACK) -let inputStyles = Skyflow.Styles(base: baseStyle) -let labelStyles = Skyflow.Styles(base: baseTextStyle) -let errorTextStyles = Skyflow.Styles(base: baseTextStyle) - -// Create Reveal Elements -let cardNumberInput = Skyflow.RevealElementInput( - token: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "cardnumber", - altText: "XXXX XXXX XXXX XXXX", - redaction: SKyflow.RedactionType.MASKED -) - -let cardNumberElement = container?.create(input: cardNumberInput) - -let cvvInput = Skyflow.RevealElementInput( - token: "89024714-6a26-4256-b9d4-55ad69aa4047", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "cvv", - altText: "XXX" -) -let cvvElement = container?.create(input: cvvInput) - -let expiryDateInput = Skyflow.RevealElementInput( - token: "a4b24714-6a26-4256-b9d4-55ad69aa4047", - inputStyles: inputStyles, - labelStyles: labelStyles, - errorTextStyles: errorTextStyles, - label: "expiryDate", - altText: "MM/YYYY" -) -let expiryDateElement = container?.create(input: expiryDateInput) - -// Can interact with these objects as a normal UIView Object and add to View - -// set error to the element -cvvElement!.setError("custom error") -// reset error to the element -cvvElement!.resetError() - -// Implement a custom Skyflow.Callback to be called on Reveal success/failure -public class RevealCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback -let revealCallback = RevealCallback() - -// Call reveal method on RevealContainer -container?.reveal(callback: revealCallback) - -``` - -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. - -#### Sample Response: -```json -{ - "success": [ { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75" - }, - { - "token": "89024714-6a26-4256-b9d4-55ad69aa4047" - }], - "errors": [ { - "id": "a4b24714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047" - } - }] -} -``` - -## Reporting a Vulnerability - -If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. +- `Skyflow/` — Skyflow SDK([README.md](Skyflow/README.md)) +- `SkyflowFlowVault/` — SkyflowFlowVault SDK([README.md](SkyflowFlowVault/README.md)) +- `SkyflowCore/` — shared internal module compiled into both SDKs (not installable on its own) +- `/Samples/` — sample apps for each SDK +## Requirements +- iOS 13+ +- Xcode 15+ (Swift 5.9 toolchain) to build diff --git a/Samples/README.md b/Samples/README.md deleted file mode 100644 index bb2588eb..00000000 --- a/Samples/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Samples - -Sample apps are organized by SDK — pick the folder for the product you use. - -## [`Skyflow/`](Skyflow/) — legacy Skyflow SDK (`pod 'Skyflow'` / `import Skyflow`) - -| Sample | Demonstrates | -|---|---| -| [CollectAndRevealSample](Skyflow/CollectAndRevealSample/) | Collecting card data with secure elements and revealing tokens (incl. card brand choice) | -| [ComposableElements](Skyflow/ComposableElements/) | Multiple fields composed into one container/row | -| [InputFormatting](Skyflow/InputFormatting/) | Format/translation patterns on collect elements | -| [UpsertFeature](Skyflow/UpsertFeature/) | Insert with upsert options | -| [Validations](Skyflow/Validations/) | Built-in and custom validation rules | - -## [`SkyflowFlowVault/`](SkyflowFlowVault/) — Skyflow FlowVault SDK (`pod 'SkyflowFlowVault'` / `import SkyflowFlowVault`) - -| Sample | Demonstrates | -|---|---| -| [UpdateDataUsingElements](SkyflowFlowVault/UpdateDataUsingElements/) | Updating vault records via collect elements with `skyflowId`; its Podfile also shows installing **both** SDKs side by side in one app | - -## Running a sample - -- Navigate to the desired sample (`Samples//`) -- In CMD, run the command, pod install -- Open the .xcworkspace file using xcode -- Click on Build and run - -Most Podfiles reference the published pods. `UpdateDataUsingElements` uses -`:path => '../../../'` to build the SDKs from this repository checkout — -useful for testing local changes. - -`Note`: -In every sample, in the Skyflow configuration, replace the following fields: -1. Replace the placeholder "" with the correct vaultId you want to connect -2. Replace the placeholder "" with the correct vaultURL -3. Implement the bearer token endpoint using server side auth SDK and service account file. - Replace the placeholder "" with the bearer token endpoint which gives the bearerToken, implemented at your backend. diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 31459b56..00000000 --- a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "9d7a641543031e54b3470012de29bf3cd9a042920b5fe71a347a8d0e07ccefe2", - "pins" : [ - { - "identity" : "skyflow-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/skyflowapi/skyflow-iOS.git", - "state" : { - "revision" : "cb30c6b6b8a36105fa4bf8ba6a879af8b777d2c7", - "version" : "1.25.1" - } - } - ], - "version" : 3 -} diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift deleted file mode 100644 index 12e3bd84..00000000 --- a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly -// (see CollectAndRevealViewController.swift), which already deliver typed responses -// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift deleted file mode 100644 index 67c28811..00000000 --- a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - the SDK now provides typed responses directly -// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer -// needs to declare its own local response structs. diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift b/Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift deleted file mode 100644 index 548a8e4c..00000000 --- a/Samples/Skyflow/ComposableElements/ComposableElements/ExampleAPICallback.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly -// (see ViewController.swift), which already deliver typed responses -// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift b/Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift deleted file mode 100644 index 67c28811..00000000 --- a/Samples/Skyflow/ComposableElements/ComposableElements/ResponseStructs.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - the SDK now provides typed responses directly -// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer -// needs to declare its own local response structs. diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift b/Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift deleted file mode 100644 index 548a8e4c..00000000 --- a/Samples/Skyflow/InputFormatting/InputFormatting/ExampleAPICallback.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly -// (see ViewController.swift), which already deliver typed responses -// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift b/Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift deleted file mode 100644 index 67c28811..00000000 --- a/Samples/Skyflow/InputFormatting/InputFormatting/ResponseStructs.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - the SDK now provides typed responses directly -// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer -// needs to declare its own local response structs. diff --git a/Samples/Skyflow/InputFormatting/Podfile.lock b/Samples/Skyflow/InputFormatting/Podfile.lock deleted file mode 100644 index 7592f10a..00000000 --- a/Samples/Skyflow/InputFormatting/Podfile.lock +++ /dev/null @@ -1,16 +0,0 @@ -PODS: - - Skyflow (1.18.0-beta.1): - -DEPENDENCIES: - - Skyflow (= 1.18.0-beta.1) - -SPEC REPOS: - trunk: - - Skyflow - -SPEC CHECKSUMS: - Skyflow: d702f8e9beafba05e164a3f74a6f862596f41085 - -PODFILE CHECKSUM: 4958176a88d5d7c9eb37117625a3937f5da81019 - -COCOAPODS: 1.11.3 diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift b/Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift deleted file mode 100644 index 67c28811..00000000 --- a/Samples/Skyflow/UpsertFeature/UpsertFeature/ResponseStructs.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// No longer needed - the SDK now provides typed responses directly -// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer -// needs to declare its own local response structs. diff --git a/Skyflow.podspec b/Skyflow.podspec index 9c87c678..bed0385b 100644 --- a/Skyflow.podspec +++ b/Skyflow.podspec @@ -24,14 +24,14 @@ Pod::Spec.new do |spec| # CocoaPods compiles the shared core and the legacy contract layer into a single # module, so the SPM-only re-export shim is excluded. - spec.source_files = "Sources/SkyflowCore/**/*.{swift}", "Sources/Skyflow/**/*.{swift}" + spec.source_files = "SkyflowCore/Sources/**/*.{swift}", "Skyflow/Sources/**/*.{swift}" - spec.exclude_files = "Sources/Skyflow/Exports.swift" + spec.exclude_files = "Skyflow/Sources/Exports.swift" # `package` access-level declarations (shared across the SPM targets) require a # package name when compiled outside SPM. spec.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name skyflow_ios_sdk' } - spec.resource_bundles = {'Skyflow' => ['Sources/SkyflowCore/Resources/**/*.{xcassets}'] } + spec.resource_bundles = {'Skyflow' => ['SkyflowCore/Sources/Resources/**/*.{xcassets}'] } end diff --git a/Skyflow/README.md b/Skyflow/README.md new file mode 100644 index 00000000..f9fce708 --- /dev/null +++ b/Skyflow/README.md @@ -0,0 +1,2267 @@ +# Skyflow iOS SDK +--- +Skyflow's iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. + +[![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-ios/actions) +[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-ios.svg)](https://github.com/skyflowapi/skyflow-ios/releases) +[![License](https://img.shields.io/github/license/skyflowapi/skyflow-ios)](https://github.com/skyflowapi/skyflow-ios/blob/main/LICENSE) + +# Table of Contents +- [Installation](#installation) + - [Requirements](#requirements) + - [Configuration](#configuration) +- [Initializing Skyflow-iOS](#initializing-skyflow-ios) +- [Securely collecting data client-side](#securely-collecting-data-client-side) +- [Securely collecting data client-side using Composable Elements](#securely-collecting-data-client-side-using-composable-elements) +- [Securely revealing data client-side](#securely-revealing-data-client-side) + +# Installation + +## Requirements +- iOS 15.0 and above +- Xcode 15 or later (Swift 5.9 toolchain) to build + +## Configuration +--- +### SPM (Swift Package Manager) +- Go to File -> Swift Packages -> New Package Dependency (in Xcode IDE) +- Enter https://github.com/skyflowapi/skyflow-iOS.git and press ok. +- In the "Choose Package Products" dialog, check the **Skyflow** product and add it to your app target. + +### Cocoapods +- To integrate skyflow-iOS into your Xcode project using CocoaPods, specify it in your Podfile: + ``` + #Mentioning the below source will pick the podspec from Skyflow repo + #Otherwise you can add cocoapod trunk as the source + #source 'https://github.com/skyflowapi/skyflow-iOS-spec.git' + + pod 'Skyflow' + ``` + + +# Initializing skyflow-iOS +---- +Use the ```initialize()``` method to initialize a Skyflow client as shown below. +```swift +//DemoTokenProvider is an implementation of the Skyflow.TokenProvider protocol +let demoTokenProvider = DemoTokenProvider() + +let config = Skyflow.Configuration( + vaultID: , + vaultURL: , + tokenProvider: demoTokenProvider, + options: Skyflow.Options( + logLevel: Skyflow.LogLevel, // optional, if not specified default is ERROR + env: Skyflow.Env // optional, if not specified default is PROD + ) +) + +let skyflowClient = Skyflow.initialize(config) +``` + +For the tokenProvider parameter, pass in an implementation of the `Skyflow.TokenProvider` protocol that declares a getBearerToken method which retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. + +For example, if the response of the consumer tokenAPI is in the below format + +``` +{ + "accessToken": string, + "tokenType": string +} +``` + +then, your Skyflow.TokenProvider Implementation should be as below + +```swift +public class DemoTokenProvider: Skyflow.TokenProvider { + public func getBearerToken(_ apiCallback: Skyflow.Callback) { + if let url = URL(string: ) { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) {data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + apiCallback.onFailure(error) + } + } + } + task.resume() + } + } +} +``` + +NOTE: You should pass access token as `String` value in the success callback of getBearerToken. + +For `logLevel` parameter, there are 4 accepted values in Skyflow.LogLevel + +- `DEBUG` + + When `Skyflow.LogLevel.DEBUG` is passed, all level of logs will be printed(DEBUG, INFO, WARN, ERROR). + +- `INFO` + + When `Skyflow.LogLevel.INFO` is passed, INFO logs for every event that has occurred during the SDK flow execution will be printed along with WARN and ERROR logs. + +- `WARN` + + When `Skyflow.LogLevel.WARN` is passed, WARN and ERROR logs will be printed. + +- `ERROR` + + When `Skyflow.LogLevel.ERROR` is passed, only ERROR logs will be printed. + +`Note`: + - The ranking of logging levels is as follows : DEBUG < INFO < WARN < ERROR + - since `logLevel` is optional, by default the logLevel will be `ERROR`. + + +For `env` parameter, there are 2 accepted values in Skyflow.Env + +- `PROD` +- `DEV` + + In [Event Listeners](#event-listener-on-collect-elements), actual value of element can only be accessed inside the handler when the `env` is set to `DEV`. + +`Note`: + - since `env` is optional, by default the env will be `PROD`. + - Use `env` option with caution, make sure the env is set to `PROD` when using `skyflow-iOS` in production. + + +--- +# Securely collecting data client-side +- [**Inserting data into the vault**](#insert-data-into-the-vault) +- [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) +- [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) +- [**Using validations on Collect Elements**](#validations) +- [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) +- [**UI Error for Collect Elements**](#ui-error-for-collect-elements) +- [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) + +## Insert data into the vault + + To insert data into your vault, use the `skyflowClient.insert()` method. + ```swift + insert(records: [String: Any], options: InsertOptions?= InsertOptions() , callback: Skyflow.Callback) + ``` + The `insert()` method requires a records parameter. + The `records` parameter takes an array of records to insert into the vault. + +#### Insert call example + ```swift + let insertCallback = InsertCallback() //A Custom callback using Skyflow.Callback + skyflowClient.insert( + records: [ + "records": [ + [ + "table": "cards", + "fields": [ + "cardNumber": "41111111111", + "cvv": "123", + ] + ] + ] + ], + callback: insertCallback +) +``` +Skyflow returns tokens for the record you just inserted. +```json +{ + "records": [ { + "table": "cards", + "fields": { + "cardNumber": "f37186-e7e2-466f-91e5-48e12c2bcbc1", + "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" + } + }] +} +``` + + The `options` parameter takes a Skyflow.InsertOptions object. + + InsertOptions includes a `tokens` boolean that controls whether you receive tokens after inserting a record into the vault. + +InsertOptions also supports the **upsert** feature, that lets you conditionally insert or update an existing record by specifying the unique column to use as the unique value. + +For example, if you specify the ‘customer_id’ column to use for upsert, and the same customer_id already exists, the existing record will update. If the customer_id doesn't exist, the vault creates a new record. + +#### Insert call example +```swift +let records = [ + "records" : [ + [ + "table": "customers", //The table where you are inserting the record. + "fields": [ + "name" : "Francis", + "customer_id" : "12345" + ] + ] + ] +] + +let upsertOptions = [["table": "customers", "column": "customer_id"]] as [[String : Any]] +let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) +let insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.Callback + +skyflowClient.insert(records: records, options: insertOptions, callback: insertCallback) +``` + +## Using Skyflow Elements to collect data + +**Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements in your application. + +### Step 1: Create a container + +First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below + +```swift +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element +To create a collect Element, we must first construct a Skyflow.CollectElementInput object defined as shown below: + +```swift +let collectElementInput = Skyflow.CollectElementInput( + table: String, // optional, the table this data belongs to + column: String, // optional, the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element + type: Skyflow.ElementType, // Skyflow.ElementType enum +) +``` +The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) +- `table` and `column` are optional only if the element is being used in invokeConnection() + +The `inputStyles` parameter accepts a Skyflow.Styles object which consists of multiple `Skyflow.Styles` objects which should be applied to the form element in the following states: + +- `base`: all other variants inherit from these styles +- `complete`: applied when the Element has valid input +- `empty`: applied when the Element has no input +- `focus`: applied when the Element has focus +- `invalid`: applied when the Element has invalid input + +Each Style object accepts the following properties, please note that each property is optional: + +```swift +let style = Skyflow.Style( + borderColor: UIColor, // optional + cornerRadius: CGFloat, // optional + padding: UIEdgeInsets, // optional + borderWidth: CGFloat, // optional + font: UIFont, // optional + textAlignment: NSTextAlignment, // optional + textColor: UIColor, // optional + boxShadow: CALayer, // optional + backgroundColor: UIColor, // optional + minWidth: CGFloat, // optional + maxWidth: CGFloat, // optional + minHeight: CGFloat, // optional + maxHeight: CGFloat, // optional + cursorColor: UIColor, // optional + width: CGFloat, // optional + height: CGFloat, // optional + placeholderColor: UIColor, // optional + cardIconAlignment: CardIconAlignment // optional, Skyflow.CardIconAlignment enum, default is .left +) +``` + +An example Skyflow.Styles object +```swift +let styles = Skyflow.Styles( + base: style, // optional + complete: style, // optional + empty: style, // optional + focus: style, // optional + invalid: style, // optional + requiredAsterisk: style // optional +) +``` + +The `labelStyles` and `errorTextStyles` fields accept the above mentioned `Skyflow.Styles` object which are applied to the `label` and `errorText` text views respectively. + +The states that are available for `labelStyles` are `base` and `focus`. + +`requiredAsterisk`: Styles applied for the Asterisk symbol in the label. Defaults to red. + +The state that is available for `errorTextStyles` is only the `base` state, it shows up when there is some error in the collect element. + +The parameters in `Skyflow.Style` object that are respected for `label` and `errorText` text views are +- padding +- font +- textColor +- textAlignment + +Other parameters in the `Skyflow.Style` object are ignored for `label` and `errorText` text views. + +Finally, the `type` parameter takes a Skyflow.ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 8 types: +- `INPUT_FIELD` +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `CVV` +- `PIN` +- `EXPIRATION_YEAR` +- `EXPIRATION_MONTH` + + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. See the section on [validations](#validations) for more information on validations. + +Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. + +```swift +Skyflow.CollectElementOptions( + required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: String, // Format for the element + translation: [Character: String] // Indicates the allowed data type value for format. + enableCopy: Boolean, // Indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' + cardMetaData: [String: [Skyflow.CardType]] // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). +) +``` + +- `required`: Indicates whether the field is marked as required or not. Default is `false`. +- `enableCardIcon`: Indicates whether the icon is visible for the CARD_NUMBER element. Default is `true`. +- `format`: A string value that indicates the format pattern applicable to the element type. Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. + For INPUT_FIELD elements, + - the length of `format` determines the expected length of the user input. + - if `translation` isn't specified, the `format` value is considered a string literal. +- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. +- `enableCopy`: Indicates whether to enable the copy icon in collect elements to copy text to clipboard. +- `cardMetaData`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow-supported card types and determines which brands display in the card number element's card brand choice dropdown. `Skyflow.CardType` is an enum with all Skyflow-supported card schemes. + +```swift +import Skyflow + +const cardMetaData = [ "scheme": [Skyflow.CardType]] // Optional, array of skyflow supported card types. +``` + +#### Supported card types by Skyflow.CardType : +- `VISA` +- `MASTERCARD` +- `AMEX` +- `DINERS_CLUB` +- `DISCOVER` +- `JCB` +- `MAESTRO` +- `UNIONPAY` +- `HIPERCARD` +- `CARTES_BANCAIRES` + +Update cardMetaData: +```swift +cardNumberElement.update(updateOptions: CollectElementOptions(cardMetaData: ["scheme": [Skyflow.CardType.CARTES_BANCAIRES, Skyflow.CardType.MASTERCARD]])) +``` + +Accepted values by element type: + +| Element type | `format`and `translation` values | Examples | +| -------------| ------------------------------- | -------- | +| EXPIRATION_DATE |
  • `format`
    • `mm/yy`(default)
    • `mm/yyyy`
    • `yy/mm`
    • `yyyy/mm`
    |
    • 12/27
    • 12/2027
    • 27/12
    • 2027/12
| +| EXPIRATION_YEAR |
  • `format`
    • `yyyy` (default)
    • `yy`
|
  • 27
  • 2027
| +| CARD_NUMBER |
  • `format`
    • `XXXX XXXX XXXX XXXX` (default)
    • `XXXX-XXXX-XXXX-XXXX`
|
  • 1234 5678 9012 3456
  • 1234-5678-9012-3456
| +| INPUT_FIELD |
  • `format`: A string that matches the desired output, with placeholder characters of your choice.
  • `translation`: A dictionary of key/value pairs. Defaults to `[ "X": "[0-9]"]`
| With a `format` of `+91 XXXX-XX-XXXX` and a `translation` of `[ "X": "[0-9]"]`, user input of "1234121234" displays as "+91 1234-12-1234". | + +Collect Element Options examples for INPUT_FIELD +Example 1 +```swift +Skyflow.CollectElementOptions( + required: true, + enableCardIcon: true, + format: "+91 XXXX-XX-XXXX", + translation: [ "X": "[0-9]"] +) +``` +User input: "1234121234" + +Value displayed in INPUT_FIELD: "+91 1234-12-1234" + +Example 2 +```swift +Skyflow.CollectElementOptions( + required: true, + enableCardIcon: true, + format: "AY XX-XXX-XXXX", + translation: [ "X": "[0-9]", "Y": "[A-Z]"] +) +``` +User input: "B1234121234" + +Value displayed in INPUT_FIELD: "AB 12-341-2123" + +Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `Skyflow.CollectElementInput` object as defined above and the `options` parameter takes an `Skyflow.CollectElementOptions` object as described below: + +```swift +let collectElementInput = Skyflow.CollectElementInput( + table: String, // the table this data belongs to + column: String, // the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element + type: Skyflow.ElementType, // Skyflow.ElementType enum +) + +let collectElementOptions = Skyflow.CollectElementOptions( + required: false, // indicates whether the field is marked as required. Defaults to 'false', + enableCardIcon: true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: "mm/yy" // format for the element, + enableCopy: true, // indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' +) + +let element = container?.create(input: collectElementInput, options: collectElementOptions) +``` + + + +### Step 3: Mount Elements to the Screen + +To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add it as a subview programmatically. + +```swift +let stackView = UIStackView() +stackView.addArrangedSubview(element) +``` + +The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state + +``` swift +func clearFieldsOnSubmit(_ elements: [TextField]) { + // resets all elements in the array + for element in elements { + element.unmount() + } +} +``` + +#### Step 4 : Collect data from Elements +When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: +```swift +// Non-PCI records +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +// Upsert +let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) + +let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) + +//Custom callback - implementation of Skyflow.callback +let insertCallback = InsertCallback() +container?.collect(callback: insertCallback, options: options) + +``` + +### Collect data with Skyflow Elements + +#### Collect call example: +```swift + +//Initialize skyflow configuration. +let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = Skyflow.initialize(config) + +//Create a CollectContainer. +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) + +//Create Skyflow.Styles with individual Skyflow.Style variants. +let baseStyle = Skyflow.Style(borderColor: UIColor.blue) +let baseTextStyle = Skyflow.Style(textColor: UIColor.black) +let completeStyle = Skyflow.Style(borderColor: UIColor.green) +let focusTextStyle = Skyflow.Style(textColor: UIColor.red) +let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = Skyflow.Styles(base: baseTextStyle) + +let iconStyles = Skyflow.Styles(base: Style(cardIconAlignment: .left)) + +// Create a CollectElementInput. +let input = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + iconStyles: iconStyles, + label: "card number", + placeholder: "card number", + type: Skyflow.ElementType.CARD_NUMBER +) + +// Create an option to require the element. +let requiredOption = Skyflow.CollectElementOptions(required: true, enableCopy: true) + +// Create a Collect Element from the Collect Container. +let skyflowElement = container?.create(input: input, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View + +// Non-PCI records +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] + + //Upsert options + let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) + + +//Implement a custom Skyflow.Callback to call on Insertion success/failure. +public class InsertCallback: Skyflow.Callback { + public func onSuccess(_ responseBody: Any) { + print(responseBody) + } + public func onFailure(_ error: Any) { + print(error) + } +} + + +// Initialize custom Skyflow.Callback. +let insertCallback = InsertCallback() + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` + +#### Skyflow returns tokens for the record you just inserted: +``` +{ + "records": [ { + "table": "cards", + "fields": { + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" + } + }, { + "table": "persons", + "fields": { + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + } + }] +} +``` + +## Using Skyflow Elements to update data +You can update the data in a vault with Skyflow Elements. Use the following steps to securely update data. + +### Step 1: Create a container + +First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below + +```swift +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element +To create a collect Element, we must first construct a Skyflow.CollectElementInput object defined as shown below: + +```swift +let collectElementInput = Skyflow.CollectElementInput( + table: String, // optional, the table this data belongs to + column: String, // optional, the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element + type: Skyflow.ElementType, // Skyflow.ElementType enum + skyflowID: String, // The skyflow_id of the record to be updated. +) +``` + +The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. + +**Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) +- `table` and `column` are optional only if the element is being used in invokeConnection() + +Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. + +```swift +Skyflow.CollectElementOptions( + required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: String, // Format for the element + translation: [Character: String] // Indicates the allowed data type value for format. + enableCopy: Boolean, // Indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' + cardMetaData: [String: [Skyflow.CardType]] // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). +) +``` + +### Step 3: Mount Elements to the Screen + +To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add it as a subview programmatically. + +```swift +let stackView = UIStackView() +stackView.addArrangedSubview(element) +``` + +The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state + +``` swift +func clearFieldsOnSubmit(_ elements: [TextField]) { + // resets all elements in the array + for element in elements { + element.unmount() + } +} +``` + +### Step 4 : Update data from Elements +When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: +- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' +- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. +- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. + +```swift +// Non-PCI records +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowID": "value"]]] +// Upsert +let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) + +let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) + +//Custom callback - implementation of Skyflow.callback +let insertCallback = InsertCallback() +container?.collect(callback: insertCallback, options: options) +``` + +**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. + +### End to end example of updating data with Skyflow Elements +```swift + +//Initialize skyflow configuration. +let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = Skyflow.initialize(config) + +//Create a CollectContainer. +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) + +//Create Skyflow.Styles with individual Skyflow.Style variants. +let baseStyle = Skyflow.Style(borderColor: UIColor.blue) +let baseTextStyle = Skyflow.Style(textColor: UIColor.black) +let completeStyle = Skyflow.Style(borderColor: UIColor.green) +let focusTextStyle = Skyflow.Style(textColor: UIColor.red) +let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = Skyflow.Styles(base: baseTextStyle) + +let iconStyles = Skyflow.Styles(base: Style(cardIconAlignment: .left)) + +// Create a CollectElementInput. +let input = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + iconStyles: iconStyles, + label: "card number", + placeholder: "card number", + type: Skyflow.ElementType.CARD_NUMBER, + skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" +) + +// Create an option to require the element. +let requiredOption = Skyflow.CollectElementOptions(required: true, enableCopy: true) + +// Create a Collect Element from the Collect Container. +let skyflowElement = container?.create(input: input, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View + +// Non-PCI records +let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882"]] + + //Upsert options + let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) + + +//Implement a custom Skyflow.Callback to call on Insertion success/failure. +public class InsertCallback: Skyflow.Callback { + public func onSuccess(_ responseBody: Any) { + print(responseBody) + } + public func onFailure(_ error: Any) { + print(error) + } +} + + +// Initialize custom Skyflow.Callback. +let insertCallback = InsertCallback() + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` + + +#### Skyflow returns tokens for the record you just updated: +```json +{ + "records": [ + { + "table": "persons", + "fields": { + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" + } + }, + { + "table": "cards", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "first_name": "131e70dc-6f76-4319-bdd3-96281e051051" + } + } + ] +} +``` + +## Validations + +Skyflow-iOS provides two types of validations on Collect Elements + +#### 1. Default Validations: +Every Collect Element except of type `INPUT_FIELD` has a set of default validations listed below: +- `CARD_NUMBER`: Card number validation with checkSum algorithm(Luhn algorithm), available card lengths for defined card types +- `CARD_HOLDER_NAME`: Name should be 2 or more symbols, valid characters should match pattern - `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$` +- `CVV`: Card CVV can have 3-4 digits +- `EXPIRATION_DATE`: Any date starting from current month. By default valid expiration date should be in short year format - `MM/YY` +- `PIN`: Can have 4-12 digits + +#### 2. Custom Validations: +Custom validations can be added to any element which will be checked after the default validations have passed. The following Custom validation rules are currently supported: +- `RegexMatchRule`: You can use this rule to specify any Regular Expression to be matched with the text field value +- `LengthMatchRule`: You can use this rule to set the minimum and maximum permissible length of the textfield value +- `ElementValueMatchRule`: You can use this rule to match the value of one element with another + +The Sample code below illustrates the usage of custom validations: + +```swift +/* + Reset Password - A simple example that illustrates custom validations. + The below code shows two input fields with custom validations, + one to enter a password and the second to confirm the same password. + */ + +var myRuleset = ValidationSet() +let strongPasswordRule = RegexMatchRule( + regex: "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]*$", + error: "At least one letter and one number" +) // This rule enforces a strong password + +let lengthRule = LengthMatchRule( + minLength: 8, + maxLength: 16, + error: "Must be between 8 and 16 digits" +) // this rule allows input length between 8 and 16 characters + +// for the Password element +myRuleset.add(rule: strongPasswordRule) +myRuleset.add(rule: lengthRule) + +let collectElementOptions = CollectElementOptions(required: true) + +let passwordInput = CollectElementInput( + inputStyles: styles, + label: "password", + placeholder: "********", + type: .INPUT_FIELD, + validations: myRuleset +) +let password = container?.create(input: passwordInput, options: collectElementOptions) + +// For confirm password element - shows error when the passwords don't match +let elementValueMatchRule = ElementValueMatchRule(element: password!, error: "passwords don't match") +let confirmPasswordInput = CollectElementInput( + inputStyles: styles, + label: "Confirm password", + placeholder: "********", + type: .INPUT_FIELD, + validations: ValidationSet(rules: [strongPasswordRule, lengthRule, elementValueMatchRule]) +) +let confirmPassword = container?.create(input: confirmPasswordInput, options: collectElementOptions) + +// mount elements on screen - errors will be shown if any of the validaitons fail +stackView.addArrangedSubview(password!) +stackView.addArrangedSubview(confirmPassword!) +``` + +## Event Listener on Collect Elements +Helps to communicate with skyflow elements by listening to an event + +```swift +element!.on(eventName: Skyflow.EventName) { _ in + // handle function +} +``` + +There are 4 events in `Skyflow.EventName` +- `CHANGE` + Change event is triggered when the Element's value changes. +- `READY` + Ready event is triggered when the Element is fully rendered +- `FOCUS` + Focus event is triggered when the Element gains focus +- `BLUR` + Blur event is triggered when the Element loses focus. +The handler ```(state: [String: Any]) -> Void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. + +```swift +let state = [ + "elementType": Skyflow.ElementType, + "isEmpty": Bool , + "isFocused": Bool, + "isValid": Bool, + "value": String, + "selectedCardScheme": String // only for CARD_NUMBER element type +] +``` +`Note:` +- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. +- `selectedCardScheme` is only populated for the `CARD_NUMBER` element state when a user chooses a card brand. By default, `selectedCardScheme` is an empty string. + +##### Sample code snippet for using listeners +```swift +// create skyflow client with loglevel:"DEBUG" +let config = Skyflow.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) +) + +let skyflowClient = Skyflow.initialize(config) + +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) + +// Create a CollectElementInput +let cardNumberInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + type: Skyflow.ElementType.CARD_NUMBER, + ) +let cardHolderNameInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardHolderName", + type: Skyflow.ElementType.CARDHOLDER_NAME, + ) + +let cardNumber = container?.create(input: cardNumberInput) +let cardHolderName = container?.create(input: cardHolderNameInput) + + +// subscribing to CHANGE event, which gets triggered when element changes +cardNumber.on(eventName: Skyflow.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} + +cardHolderName.on(eventName: Skyflow.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} + +``` +##### Sample Element state object when `env` is `DEV` +```swift +[ + "elementType": Skyflow.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "4111111111111111" +] +[ + "elementType": Skyflow.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "John" +] + +``` +##### Sample Element state object when `env` is `PROD` +```swift +[ + "elementType": Skyflow.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "41111111XXXXXXXX" +] +[ + "elementType": Skyflow.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "" +] +``` +## UI Error for Collect Elements + +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: String)` method is used to set the error text for the element, when this method is trigerred, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is trigerred on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +##### Sample code snippet for setError and resetError + +```swift +// Create skyflow client with loglevel:"DEBUG" +let config = Skyflow.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) +) + +let skyflowClient = Skyflow.initialize(config) + +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) + +// Create a CollectElementInput +let cardNumberInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + type: Skyflow.ElementType.CARD_NUMBER +) + +let cardNumber = container.create(input: cardNumberInput) + +// Set custom error +cardNumber.setError("custom error") + +// Reset custom error +cardNumber.resetError() +``` + +## Set and Clear value for Collect Elements (DEV ENV ONLY) + +`setValue(value: String)` method is used to set the value of the element. This method will override any previous value present in the element. + +`clearValue()` method is used to reset the value of the element. + +`Note:` This methods are only available in DEV env for testing/developmental purposes and MUST NOT be used in PROD env. + +##### Sample code snippet for setValue and clearValue + +```swift +// Create skyflow client with env DEV +let config = Skyflow.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: Skyflow.Options(env: Skyflow.Env.DEV) +) + +let skyflowClient = Skyflow.initialize(config) + +let container = skyflowClient.container(type: Skyflow.ContainerType.COLLECT) + +// Create a CollectElementInput +let cardNumberInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + type: Skyflow.ElementType.CARD_NUMBER +) + +let cardNumber = container.create(input: cardNumberInput) + +// Set a value programatically +cardNumber.setValue("4111111111111111") + +// Clear the value +cardNumber.clearValue() +``` + +# Securely collecting data client-side using Composable Elements +- [Using Skyflow Composable Elements to collect data](#using-skyflow-composable-elements-to-collect-data) +- [Using Skyflow Composable Elements to update data](#using-skyflow-composable-elements-to-update-data) + +## Using Skyflow Composable Elements to collect data +Composable Elements combine multiple Skyflow Elements in a single row. The following steps create a composable element and securely collect data through it. + +### Step 1: Create a composable container + +First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below + +```swift +let container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, options: ContainerOptions) +``` + +The container requires an options object that contains the following keys: + +- `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. + + For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. + + `Note`: The sum of values in the layout array should be equal to the number of elements created + +- `styles`: styles to apply to each composable row. + +- `errorTextStyles`: styles to apply if an error is encountered. + +```swift +var containerOptions = ContainerOptions( + layout: [1,1,2], // required + styles: Skyflow.Styles, // optional + errorTextStyles: Skyflow.Styles //optional + ) +``` +### Step 2: Create Composable Elements +Composable Elements use the following schema: + +```swift +let composableElementInput = Skyflow.CollectElementInput( + table: String, // optional, the table this data belongs to + column: String, // optional, the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element + type: Skyflow.ElementType, // Skyflow.ElementType enum +) +``` +The `table` and `column` fields indicate which table and column in the vault the Element correspond to. +**Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) + +The `inputStyles` parameter accepts a Skyflow.Styles object which consists of multiple `Skyflow.Styles` objects which should be applied to the form element in the following states: + +- `base`: all other variants inherit from these styles +- `complete`: applied when the Element has valid input +- `empty`: applied when the Element has no input +- `focus`: applied when the Element has focus +- `invalid`: applied when the Element has invalid input + +Each Style object accepts the following properties, please note that each property is optional: + +```swift +let style = Skyflow.Style( + borderColor: UIColor, // optional + cornerRadius: CGFloat, // optional + padding: UIEdgeInsets, // optional + borderWidth: CGFloat, // optional + font: UIFont, // optional + textAlignment: NSTextAlignment, // optional + textColor: UIColor // optional +) +``` +An example Skyflow.Styles object +```swift +let styles = Skyflow.Styles( + base: style, // optional + complete: style, // optional + empty: style, // optional + focus: style, // optional + invalid: style // optional +) +``` + +The `labelStyles` and `errorTextStyles` fields accept the above mentioned `Skyflow.Styles` object which are applied to the `label` and `errorText` text views respectively. + +The states that are available for `labelStyles` are `base` and `focus`. + +The state that is available for `errorTextStyles` is only the `base` state, it shows up when there is some error in the composable element. + +The parameters in `Skyflow.Style` object that are respected for `label` and `errorText` text views are +- padding +- font +- textColor +- textAlignment + +Other parameters in the `Skyflow.Style` object are ignored for `label` and `errorText` text views. + +Finally, the `type` parameter takes a Skyflow.ElementType. Each type applies the appropriate regex and validations to the form element. + +The iOS SDK supports the following composable elements: + +- `INPUT_FIELD` +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `CVV` +- `PIN` +- `EXPIRATION_YEAR` +- `EXPIRATION_MONTH` + +**Note**: Only when the entered value in the below composable elements is valid, the focus shifts automatically. The element types are: + +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. See the section on [validations](#validations) for more information on validations. + +Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. + +```swift +Skyflow.CollectElementOptions( + required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: String, // Format for the element + translation: [Character: String] // Indicates the allowed data type value for format. +) +``` +- `required`: Indicates whether the field is marked as required or not. Default is `false`. +- `enableCardIcon`: Indicates whether the icon is visible for the CARD_NUMBER element. Default is `true`. +- `format`: A string value that indicates the format pattern applicable to the element type. Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. + For INPUT_FIELD elements, + - the length of `format` determines the expected length of the user input. + - if `translation` isn't specified, the `format` value is considered a string literal. +- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. + +The accepted EXPIRATION_DATE values are + +- `mm/yy` (default) +- `mm/yyyy` +- `yy/mm` +- `yyyy/mm` + +The accepted EXPIRATION_YEAR values are + +- `yy` (default) +- `yyyy` + +Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `Skyflow.CollectElementInput` object as defined above and the `options` parameter takes an `Skyflow.CollectElementOptions` object as described below: + +```swift +let composableElementInput = Skyflow.CollectElementInput( + table: String, // the table this data belongs to + column: String, // the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element + type: Skyflow.ElementType, // Skyflow.ElementType enum +) + +let collectElementOptions = Skyflow.CollectElementOptions( + required: false, // indicates whether the field is marked as required. Defaults to 'false', + enableCardIcon: true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: "mm/yy" // Format for the element +) + +let element = container?.create(input: composableElementInput, options: collectElementOptions) +``` +### Step 3: Mount Elements to the Screen + +To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add composable elements view using `container?.getComposableView()`, it as a subview programmatically. + +```swift +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +``` + +The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state + +``` swift +func clearFieldsOnSubmit(_ elements: [TextField]) { + // resets all elements in the array + for element in elements { + element.unmount() + } +} +``` +### Step 4: Collect data from elements +When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. +The options parameter takes a `Skyflow.CollectOptions` object as shown below: + +- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' +- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. +- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. + +```swift +// Non-PCI records +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +// Upsert +let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) + +let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) + +//Custom callback - implementation of Skyflow.callback +let insertCallback = InsertCallback() +container?.collect(callback: insertCallback, options: options) +``` +End to end example of collecting data with Composable Elements + +```swift + +//Initialize skyflow configuration. +let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = Skyflow.initialize(config) + +let containerOptions = ContainerOptions(layout: [1,2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) + +//Create Skyflow.Styles with individual Skyflow.Style variants. +let baseStyle = Skyflow.Style(borderColor: UIColor.blue) +let baseTextStyle = Skyflow.Style(textColor: UIColor.black) +let completeStyle = Skyflow.Style(borderColor: UIColor.green) +let focusTextStyle = Skyflow.Style(textColor: UIColor.red) +let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = Skyflow.Styles(base: baseTextStyle) + +// Create Composable Elements. + +let cardHolderNameElementInput = Skyflow.CollectElementInput( + table: "cards", + column: "first_name", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "first name", + placeholder: "first name", + type: Skyflow.ElementType.CARDHOLDER_NAME +) +// Create an option to require the element. +let requiredOption = Skyflow.CollectElementOptions(required: true) + +// Create a Composable Element from the Composable Container. +let cardHolderNameElement = container?.create(input: cardHolderNameElementInput, options: requiredOption) + + +let cardNumberElementInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "card number", + placeholder: "card number", + type: Skyflow.ElementType.CARD_NUMBER +) + +let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) + +let cvvElementInput = Skyflow.CollectElementInput( + table: "cards", + column: "cvv", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cvv", + placeholder: "cvv", + type: Skyflow.ElementType.CVV +) + +let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} + +// Non-PCI records +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] + + //Upsert options +let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) + + +//Implement a custom Skyflow.Callback to call on Insertion success/failure. +public class InsertCallback: Skyflow.Callback { + public func onSuccess(_ responseBody: Any) { + print(responseBody) + } + public func onFailure(_ error: Any) { + print(error) + } +} + +// Initialize custom Skyflow.Callback. +let insertCallback = InsertCallback() + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` +### Sample Response: + +``` +{ + "records": [ { + "table": "cards", + "fields": { + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" + } + }, { + "table": "persons", + "fields": { + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + } + }] +} +``` +[For information on validations, see validations.](#validations) + + + +## Set an event listener on Composable Elements: +You can communicate with Skyflow Elements by listening to element events: + +```swift +element!.on(eventName: Skyflow.EventName) { _ in + // handle function +} +``` + +The SDK supports four events: + +- CHANGE: Triggered when the Element's value changes. +- READY: Triggered when the Element is fully rendered. +- FOCUS: Triggered when the Element gains focus. +- BLUR: Triggered when the Element loses focus. + +The handler ```(state: [String: Any]) -> Void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. + +```swift +let state = [ + "elementType": Skyflow.ElementType, + "isEmpty": Bool , + "isFocused": Bool, + "isValid": Bool, + "value": String +] +``` + +`Note`: +values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. + +#### Example Usage of Event Listener on Composable Elements + +```swift +// create skyflow client with loglevel:"DEBUG" +let config = Skyflow.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) +) + +let skyflowClient = Skyflow.initialize(config) + +let containerOptions = ContainerOptions(layout: [2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) + +// Create a CollectElementInput +let cardNumberInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + type: Skyflow.ElementType.CARD_NUMBER, + ) +let cardHolderNameInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardHolderName", + type: Skyflow.ElementType.CARDHOLDER_NAME, + ) + +let cardNumber = container?.create(input: cardNumberInput) +let cardHolderName = container?.create(input: cardHolderNameInput) + +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +// subscribing to CHANGE event, which gets triggered when element changes +cardNumber.on(eventName: Skyflow.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} + +cardHolderName.on(eventName: Skyflow.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} +``` + +##### Sample Element state object when `env` is `DEV` +```swift +[ + "elementType": Skyflow.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "4111111111111111" +] +[ + "elementType": Skyflow.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "John" +] + +``` +##### Sample Element state object when `env` is `PROD` +```swift +[ + "elementType": Skyflow.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "41111111XXXXXXXX" +] +[ + "elementType": Skyflow.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "" +] +``` +### Update composable elements +You can update composable element properties with the `update` interface. + +The `update` interface takes the below object: +```swift +let updateElement = Skyflow.CollectElementInput( + table: String, // optional the table this data belongs to + column: String, // optional the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element +) +``` +Only include the properties that you want to update for the specified composable element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +`Note`: You can't update the type property of an element. + +### End to end example +```swift +// create skyflow client with loglevel:"DEBUG" +let config = Skyflow.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) +) + +let skyflowClient = Skyflow.initialize(config) + +let containerOptions = ContainerOptions(layout: [2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) + +// Create a CollectElementInput +let cardNumberInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + type: Skyflow.ElementType.CARD_NUMBER, + ) +let cardHolderNameInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardHolderName", + type: Skyflow.ElementType.CARDHOLDER_NAME, + ) + +let cardNumber = container?.create(input: cardNumberInput) +let cardHolderName = container?.create(input: cardHolderNameInput) + +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +// Update table, column, inputStyles properties on cardNumber. +cardNumber.update(update: CollectElementInput( + table: "cards", + column: "cardHolderName", + inputStyles: Skyflow.Styles(base: Style(borderColor: UIColor.red)) +)) +let lengthRule = LengthMatchRule( + minLength: 5, + maxLength: 16, + error: "Must be between 5 and 16 digits") + +// Update validations and placeholder property on cardHolderName. +cardHolderName.update(update: CollectElementInput( + placeholder: "cardHolderName", + validations: ValidationSet(rules: [lengthRule])) +)) +``` + +### Set an event listener on a composable container + +Currently, the SDK supports one event: + + - `SUBMIT`: Triggered when the Enter key is pressed in any container element. + +The handler function(void) -> void is a callback function you provide that's called when the `SUBMIT` event fires. + +#### Example + +```swift +let containerOptions = ContainerOptions(layout: [1], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let composableContainer = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) + +// Creating the element. +let cardNumberInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + type: Skyflow.ElementType.CARD_NUMBER, + ) +let cardNumber = composableContainer?.create(input: cardNumberInput) + +// get composable view and mount it +let stackView = UIStackView() +do { + let composableView = try composableContainer?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +// Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. +composableContainer?.on(eventName: .SUBMIT){ + // Your implementation when the SUBMIT(enter) event occurs. + print('Submit Event Listener is being Triggered.') +} + +``` + +## Using Skyflow Composable Elements to update data +Composable Elements combine multiple Skyflow Elements in a single row. The following steps create a composable element and securely update data through it. + +### Step 1: Create a composable container +First create a **container** for the form elements using the ```skyflowClient.container(type: Skyflow.ContainerType)``` method as show below + +```swift +var containerOptions = ContainerOptions( + layout: [1,1,2], // required + styles: Skyflow.Styles, // optional + errorTextStyles: Skyflow.Styles //optional + ) +let container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, options: ContainerOptions) +``` + +### Step 2: Create Composable Elements +Composable Elements use the following schema: + +```swift +let composableElementInput = Skyflow.CollectElementInput( + table: String, // optional, the table this data belongs to + column: String, // optional, the column into which this data should be inserted + inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element + labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element + type: Skyflow.ElementType, // Skyflow.ElementType enum + skyflowID: String, // The skyflow_id of the record to be updated. +) +``` +The `table` and `column` fields indicate which table and column in the vault the Element correspond to. +**Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) + +Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. + +```swift +Skyflow.CollectElementOptions( + required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: String, // Format for the element + translation: [Character: String] // Indicates the allowed data type value for format. +) +``` + + +Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `Skyflow.CollectElementInput` object as defined above and the `options` parameter takes an `Skyflow.CollectElementOptions` object as described below: + + ```swift +let element = container?.create(input: composableElementInput, options: collectElementOptions) +``` +### Step 3: Mount Elements to the Screen + +To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add composable elements view using `container?.getComposableView()`, it as a subview programmatically. + +```swift +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +``` + +The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state + +``` swift +func clearFieldsOnSubmit(_ elements: [TextField]) { + // resets all elements in the array + for element in elements { + element.unmount() + } +} +``` +### Step 4: Update data from Elements +When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. +The options parameter takes a `Skyflow.CollectOptions` object as shown below: + +- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' +- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. +- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. + +```swift +// Non-PCI records +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +// Upsert +let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) + +let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) + +//Custom callback - implementation of Skyflow.callback +let insertCallback = InsertCallback() +container?.collect(callback: insertCallback, options: options) +``` +End to end example of collecting data with Composable Elements + +```swift + +//Initialize skyflow configuration. +let config = Skyflow.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = Skyflow.initialize(config) + +let containerOptions = ContainerOptions(layout: [1,2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) + +//Create Skyflow.Styles with individual Skyflow.Style variants. +let baseStyle = Skyflow.Style(borderColor: UIColor.blue) +let baseTextStyle = Skyflow.Style(textColor: UIColor.black) +let completeStyle = Skyflow.Style(borderColor: UIColor.green) +let focusTextStyle = Skyflow.Style(textColor: UIColor.red) +let inputStyles = Skyflow.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = Skyflow.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = Skyflow.Styles(base: baseTextStyle) + +// Create Composable Elements. + +let cardHolderNameElementInput = Skyflow.CollectElementInput( + table: "cards", + column: "first_name", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "first name", + placeholder: "first name", + type: Skyflow.ElementType.CARDHOLDER_NAME, + skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" +) +// Create an option to require the element. +let requiredOption = Skyflow.CollectElementOptions(required: true) + +// Create a Composable Element from the Composable Container. +let cardHolderNameElement = container?.create(input: cardHolderNameElementInput, options: requiredOption) + + +let cardNumberElementInput = Skyflow.CollectElementInput( + table: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "card number", + placeholder: "card number", + type: Skyflow.ElementType.CARD_NUMBER, + skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" +) + +let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) + +let cvvElementInput = Skyflow.CollectElementInput( + table: "cards", + column: "cvv", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cvv", + placeholder: "cvv", + type: Skyflow.ElementType.CVV, + skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" +) + +let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} + +// Non-PCI records +let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf"]] + + //Upsert options + let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) + +//Implement a custom Skyflow.Callback to call on Insertion success/failure. +public class InsertCallback: Skyflow.Callback { + public func onSuccess(_ responseBody: Any) { + print(responseBody) + } + public func onFailure(_ error: Any) { + print(error) + } +} + +// Initialize custom Skyflow.Callback. +let insertCallback = InsertCallback() + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` +### Sample Success Response: +```json +{ + "records": [ + { + "table": "persons", + "fields": { + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" + } + }, + { + "table": "cards", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", + "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" + } + } + ] +} +``` +### Sample Partial Error Response: +```json +{ + "records": [ + { + "table": "cards", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", + "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" + } + } + ], + "errors": [ + { + "error": { + "code": 400, + "message": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs. - request-id: cb397-8521-42c2-870c-92dbeec" + } + } + ] +} +``` + +# Securely revealing data client-side +- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) +- [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) +- [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) +- [**Set token for Reveal Elements**](#set-token-for-reveal-elements) +- [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) + +## Retrieving data from the vault +For non-PCI use-cases, retrieving data from the vault and revealing it in the mobile can be done either using the SkyflowID's or tokens as described below + +- ### Using Skyflow tokens + To retrieve record data using tokens, use the `detokenize(records)` method. The records parameter takes a Dictionary object that contains tokens for `record` values to fetch + ```swift + [ + "records": [ + [ + "token": String, + "redaction": Skyflow.RedactionType // Optional. Redaction to apply for retrieved data. + ] + ] + ] + ``` + Note: `redaction` defaults to [RedactionType.PLAIN_TEXT](#redaction-types). + +The following example code makes a detokenize call to reveal the masked value of a token: + +```swift + let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback + + let records = [ + "records": [ + [ + "token": "45012507-f72b-4f5c-9bf9-86b133bae719", + ], + [ + "token": "1r434532-6f76-4319-bdd3-96281e051051", + "redaction": Skyflow.RedactionType.MASKED + ] + ] + ] as [String: Any] + + skyflowClient.detokenize(records: records, callback: getCallback) + ``` + The sample response: + ```json + { + "records": [ + { + "token": "131e70dc-6f76-4319-bdd3-96281e051051", + "value": "1990-01-01" + }, + { + "token": "1r434532-6f76-4319-bdd3-96281e051051", + "value": "xxxxxxer", + } + ] + } + ``` + +- ### Using Skyflow ID's or Unique Column Values + For retrieving data from the vault, use the `get(records: JSONObject, options: GetOptions? = GetOptions(), callback: Skyflow.Callback)` method. + The `records` parameter takes a Dictionary object that contains `records` to be fetched as shown below. Each object inside array should contain: + + - Either an array of Skyflow IDs to fetch + - Or a column name and an array of column values + + The second parameter, `options`, is a `GetOptions` object that retrieves tokens of Skyflow IDs. + + Notes: + - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. + - GetOptions parameter is applicable only for retrieving tokens using Skyflow ID. + - You can't pass GetOptions along with the redaction type. + - tokens defaults to false. + + ```swift + [ + "records": [ + [ + "ids": ArrayList(), // Array of SkyflowID's of the records to be fetched + "table": String, // Name of table holding the above skyflow_id's + "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data + ], + [ + "columnValues": ArrayList(), // Array of column values of the records to be fetched + "table": String, // Name of table holding the above skyflow_id's + "columnName": String, // A unique column name + "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data + ], + ] + ] + ``` + ### Redaction Types + There are 4 accepted values in Skyflow.RedactionTypes: + - `PLAIN_TEXT` + - `MASKED` + - `REDACTED` + - `DEFAULT` + + An example of get call to fetch records: + ```swift + let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback + + let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9"] + let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] + + let columnValues = ["12345"] + let columnRecord = ["columnValues": columnValues,"columnName": "card_pin", "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] + + let invalidID = ["invalid skyflow ID"] + let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] + + let records = ["records": [record,columnRecord, badRecord]] + + skyflowClient.get(records: records, callback: getCallback) + ``` + + The sample response: + ```json + { + "records": [{ + "fields": { + "card_number": "4111111111111111", + "cvv": "127", + "expiry_date": "11/35", + "fullname": "myname", + "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" + }, + "table": "cards" + },{ + "fields": { + "card_number": "4111111111111111", + "cvv": "345", + "card_pin":"12345" + "expiry_date": "12/29", + "fullname": "joey", + "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" + }, + "table": "cards" + } + ], + "errors": [ { + "error": { + "code": "404", + "description": "No Records Found" + }, + "ids": ["invalid skyflow id"] + }] + } + ``` + + An example of get call to fetch tokens: + + ```swift + let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback + + let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9", "da26de53-95d5-4bdb-99db-8d8c66a35ff9"] + let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] + + let invalidID = ["invalid skyflow ID"] + let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] + + let records = ["records": [record, badRecord]] + + skyflowClient.get(records: records, options: GetOptions(tokens: true), callback: getCallback) + ``` + The sample Response: + ```json + { + "records": [ + { + "fields": { + "card_number": "9802-3257-3113-0294", + "expiry_date": "45012507-f72b-4f5c-9bf9-86b133bae719", + "fullname": "131e2507-f72b-4f5c-9bf9-86b133bae719", + "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" + }, + "table": "cards" + }, + { + "fields": { + "card_number": "0294-3213-3157-9802", + "expiry_date": "131e2507-f72b-4f5c-9bf9-86b133bae719", + "fullname": "45012507-f72b-4f5c-9bf9-86b133bae719", + "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" + }, + "table": "cards" + } + ], + "errors": [ + { + "error": { + "code": "404", + "description": "No Records Found" + }, + "ids": ["invalid skyflow id"] + } + ] +} + ``` + + +## Using Skyflow Elements to reveal data +Skyflow Elements can be used to securely reveal data in an application without exposing your front end to the sensitive data. This is great for use-cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. +### Step 1: Create a container +To start, create a container using the `skyflowClient.container(Skyflow.ContainerType.REVEAL)` method as shown below. +```swift + let container = skyflowClient.container(type: Skyflow.ContainerType.REVEAL) +``` + +### Step 2: Create a reveal Element +To create a reveal Element, we must first construct a Skyflow.RevealElementInput object defined as shown below: + +```swift +let revealElementInput = Skyflow.RevealElementInput( + token: String, // optional, token of the data being revealed + inputStyles: Skyflow.Styles(), // optional, styles to be applied to the element + labelStyles: Skyflow.Styles(), // optional, styles to be applied to the label of the reveal element + errorTextStyles: Skyflow.Styles(), // optional styles that will be applied to the errorText of the reveal element + label: "cardNumber", // optional, label for the element, + altText: "XXXX XXXX XXXX XXXX", // optional, string that is shown before reveal, will show token if it is not provided + redaction: Skyflow.RedactionType, // optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED +) +``` +`Note`: +- `token` is optional only if it is being used in invokeConnection() +- `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). + + +The `inputStyles` parameter accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but the only state available for a reveal element is the base state. + +The `labelStyles` and `errorTextStyles` fields accept the above mentioned `Skyflow.Styles` object as described in the [previous section](#step-2-create-a-collect-element), the only state available for a reveal element is the base state. + +The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but only a single variant is available i.e. base. + +An example of a inputStyles object: + +```swift +let inputStyles = Skyflow.Styles(base: Skyflow.Style(borderColor: UIColor.green)) +``` + +An example of a labelStyles object: + +```swift +let labelStyles = Skyflow.Styles(base: Skyflow.Style(font: UIFont (name: "GILLSANSCE-ROMAN", size: 12)))) +``` + +An example of a errorTextStyles object: + +```swift +let labelStyles = Skyflow.Styles(base: Skyflow.Style(textColor: UIColor.red)) +``` +Along with `RevealElementInput`, you can define other options in the `RevealElementOptions` object as described below: +```swift +Skyflow.RevealElementOptions( + format: String, // Format for the element. + translation: [Character: String] // Indicates the allowed data type value for format + enableCopy: Boolean, // Indicates whether to enable the copy icon in reveal elements to copy text to clipboard. Defaults to 'false' +) +``` +- `format`: A string value that indicates how the element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified, the `format` value is considered a string literal. +- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `[ "X": "[0-9]"]`. +- `enableCopy`: Indicates whether to enable the copy icon in reveal elements to copy text to clipboard. + +Reveal Element Options examples: + +Example 1: +```swift +Skyflow.RevealElementOptions( + format: "(XXX) XXX-XXXX", + translation: [ "X": "[0-9]"] +) +``` +Value from vault: "1234121234" + +Value displayed in element: "(123) 412-1234" + +Example 2: +```swift +Skyflow.RevealElementOptions( + format: "XXXX-XXXXXX-XXXXX", + translation: [ "X": "[0-9]"] +) +``` +Value from vault: "374200000000004" + +Value displayed in element: "3742-000000-00004" + +Once you've defined a `Skyflow.RevealElementInput` object and `Skyflow.RevealElementOptions`, you can use the `create()` method of the container to create the Element as shown below: + +```swift +let element = container.create(input: revealElementInput, options: Skyflow.RevealElementOptions(format: "XXXX-XXXXXX-XXXXX", +translation: ["X": "[0-9]"] +)) +``` + +### Step 3: Mount Elements to the Screen + +Elements used for revealing data are mounted to the screen the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-screen). + +### Step 4: Reveal data +When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: +```swift +let revealCallback = RevealCallback() // Custom callback - implementation of Skyflow.Callback +container.reveal(callback: revealCallback) +``` + +### UI Error for Reveal Elements + +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: String)` method is used to set the error text for the element, when this method is trigerred, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is trigerred on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +### Set token for Reveal Elements + +The `setToken(value: String)` method can be used to set the token of the Reveal Element. If no altText is set, the set token will be displayed on the UI as well. If altText is set, then there will be no change in the UI but the token of the element will be internally updated. + +### Set and Clear altText for Reveal Elements + +The `setAltText(value: String)` method can be used to set the altText of the Reveal Element. This will cause the altText to be displayed in the UI regardless of whether the token or value is currently being displayed. + +`clearAltText()` method can be used to clear the altText, this will cause the element to display the token or actual value of the element. If the element has no token, the element will be empty. + +### Reveal data with Skyflow Elements +#### End-to-end example of revealing data with Skyflow Elements +```swift +// Initialize skyflow configuration +let config = Skyflow.Configuration(vaultID: , vaultURL: , tokenProvider: demoTokenProvider) + +// Initialize skyflow client +let skyflowClient = Skyflow.initialize(config) + +// Create a Reveal Container +let container = skyflowClient.container(type: Skyflow.ContainerType.REVEAL) + +// Create Skyflow.Styles with individual Skyflow.Style variants +let baseStyle = Skyflow.Style(borderColor: UIColor.blue) +let baseTextStyle = Skyflow.Style(textColor: UIColor.BLACK) +let inputStyles = Skyflow.Styles(base: baseStyle) +let labelStyles = Skyflow.Styles(base: baseTextStyle) +let errorTextStyles = Skyflow.Styles(base: baseTextStyle) + +// Create Reveal Elements +let cardNumberInput = Skyflow.RevealElementInput( + token: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cardnumber", + altText: "XXXX XXXX XXXX XXXX", + redaction: SKyflow.RedactionType.MASKED +) + +let cardNumberElement = container?.create(input: cardNumberInput) + +let cvvInput = Skyflow.RevealElementInput( + token: "89024714-6a26-4256-b9d4-55ad69aa4047", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cvv", + altText: "XXX" +) +let cvvElement = container?.create(input: cvvInput) + +let expiryDateInput = Skyflow.RevealElementInput( + token: "a4b24714-6a26-4256-b9d4-55ad69aa4047", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "expiryDate", + altText: "MM/YYYY" +) +let expiryDateElement = container?.create(input: expiryDateInput) + +// Can interact with these objects as a normal UIView Object and add to View + +// set error to the element +cvvElement!.setError("custom error") +// reset error to the element +cvvElement!.resetError() + +// Implement a custom Skyflow.Callback to be called on Reveal success/failure +public class RevealCallback: Skyflow.Callback { + public func onSuccess(_ responseBody: Any) { + print(responseBody) + } + public func onFailure(_ error: Any) { + print(error) + } +} + +// Initialize custom Skyflow.Callback +let revealCallback = RevealCallback() + +// Call reveal method on RevealContainer +container?.reveal(callback: revealCallback) + +``` + +The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. + +#### Sample Response: +```json +{ + "success": [ { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75" + }, + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047" + }], + "errors": [ { + "id": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": { + "code": 404, + "description": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047" + } + }] +} +``` + +## Reporting a Vulnerability + +If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. \ No newline at end of file diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift similarity index 97% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift index 9c7c742b..270ff189 100644 --- a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift +++ b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift @@ -39,7 +39,7 @@ class CardBrandChoiceSampleViewController: UIViewController { let iconStyles = Skyflow.Styles(base: Style(cardIconAlignment: .right)) // create card number element - let collectCardNumberInput = Skyflow.CollectElementInput(tableName: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: Skyflow.ElementType.CARD_NUMBER) + let collectCardNumberInput = Skyflow.CollectElementInput(table: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: Skyflow.ElementType.CARD_NUMBER) let requiredOption = Skyflow.CollectElementOptions(required: true, enableCardIcon: true, enableCopy: false) collectCardNumberElement = container?.create(input: collectCardNumberInput, options: requiredOption) diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift similarity index 68% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 9f334fac..48368c2d 100644 --- a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -22,12 +22,6 @@ class CollectAndRevealViewController: UIViewController { private var revealButton: UIButton! private var revealed = false - private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] - - // Manual/visual check for the mock-CVV feature: shows the CVV token straight from - // collect()'s response so you can see it's a short numeric mock (not a real vault token) - // after submitting, without needing to inspect console output. - private var cvvMockCheckLabel: UILabel? override func viewDidLoad() { @@ -73,7 +67,7 @@ class CollectAndRevealViewController: UIViewController { ) let collectCardNumberInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", @@ -81,7 +75,7 @@ class CollectAndRevealViewController: UIViewController { type: Skyflow.ElementType.CARD_NUMBER ) let collectNameInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -89,7 +83,7 @@ class CollectAndRevealViewController: UIViewController { type: Skyflow.ElementType.CARDHOLDER_NAME ) let collectCVVInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", @@ -97,7 +91,7 @@ class CollectAndRevealViewController: UIViewController { type: .CVV ) let collectExpMonthInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -105,7 +99,7 @@ class CollectAndRevealViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -122,14 +116,6 @@ class CollectAndRevealViewController: UIViewController { collectButton.backgroundColor = .blue collectButton.setTitle("Submit", for: .normal) collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) - - let cvvMockCheckLabel = UILabel() - cvvMockCheckLabel.text = "CVV token will appear here after submit" - cvvMockCheckLabel.numberOfLines = 0 - cvvMockCheckLabel.textColor = .darkGray - cvvMockCheckLabel.font = .systemFont(ofSize: 13) - self.cvvMockCheckLabel = cvvMockCheckLabel - self.revealContainer = skyflow?.container(type: Skyflow.ContainerType.REVEAL, options: nil) self.revealButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) revealButton.backgroundColor = .blue @@ -141,23 +127,23 @@ class CollectAndRevealViewController: UIViewController { stackView.addArrangedSubview(collectExpMonth!) stackView.addArrangedSubview(collectExpYear!) stackView.addArrangedSubview(collectButton) - + stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 5 stackView.alignment = .fill stackView.translatesAutoresizingMaskIntoConstraints = false - + let scrollView = UIScrollView(frame: .zero) scrollView.isScrollEnabled = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) - + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2).isActive = true scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 2).isActive = true scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - + scrollView.addSubview(stackView) stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true @@ -167,51 +153,26 @@ class CollectAndRevealViewController: UIViewController { } } @objc func revealForm() { - let revealCallback = Skyflow.RevealCallback( - onSuccess: { (response: Skyflow.RevealResponse) in - for record in response.records { - if let error = record.error { - print("reveal failed:", error, "httpCode:", record.httpCode) - } else { - print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) - } - } - }, - onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } - ) - let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) - self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) + self.revealContainer?.reveal(callback: ExampleAPICallback()) } @objc func submitForm() { - let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: Skyflow.CollectResponse) { + internal func updateSuccess(_ response: SuccessResponse) { print(response) retryCount = 0 - for result in response.records { - if let error = result.error { - print("Record failed:", error, "httpCode:", result.httpCode) - } - } - if let tokens = response.records.first?.tokens { - updateRevealInputs(tokens: tokens) - } + updateRevealInputs(tokens: response.records[0].fields) print("Successfully got response:", response) } - internal func updateFailure(error: Skyflow.SkyflowError) { - if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Any) { + if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") - } - // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. - internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { - guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } - return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + print("Failed Operation", error) } - internal func updateRevealInputs(tokens: [String: Any]) { + internal func updateRevealInputs(tokens: Fields) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -227,48 +188,40 @@ class CollectAndRevealViewController: UIViewController { } else { self.revealed = true } - // tokenGroupRedactions: request-level redaction per token group, applied when - // reveal() is called (see revealForm()) - not per reveal element. - self.tokenGroupRedactions = [] - func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { - if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { - self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) - } - } - addTokenGroupRedaction("REDACTED", forColumn: "card_number") - addTokenGroupRedaction("MASKED", forColumn: "cvv") - addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") - addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") - let revealCardNumberInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "card_number").token, + token: tokens.card_number, inputStyles: revealStyles, - label: "Card Number" + label: "Card Number", + redaction: .REDACTED ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "cvv").token, + token: tokens.cvv, inputStyles: revealStyles, - label: "CVV" + label: "CVV", + redaction: .MASKED ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "cardholder_name").token, + token: tokens.cardholder_name, inputStyles: revealStyles, - label: "Card Holder Name" + label: "Card Holder Name", + redaction: .DEFAULT + ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "expiry_month").token, + token: tokens.expiry_month, inputStyles: revealStyles, - label: "Expiration Month" + label: "Expiration Month", + redaction: .PLAIN_TEXT ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "expiry_year").token, + token: tokens.expiry_year, inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift new file mode 100644 index 00000000..0d8be466 --- /dev/null +++ b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleAPICallback: Skyflow.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Info.plist b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/Info.plist rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist diff --git a/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift new file mode 100644 index 00000000..959044f5 --- /dev/null +++ b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let skyflow_id: String +} diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift diff --git a/Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift b/Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift rename to Skyflow/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift diff --git a/Skyflow/Samples/CollectAndRevealSample/Podfile b/Skyflow/Samples/CollectAndRevealSample/Podfile new file mode 100644 index 00000000..a02fa6bd --- /dev/null +++ b/Skyflow/Samples/CollectAndRevealSample/Podfile @@ -0,0 +1,13 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +source 'https://github.com/CocoaPods/Specs.git' + +target 'CollectAndRevealSample' do + #use_frameworks! + pod 'AEXML', '4.6.1' + + # Pods for skyflow-iOS-sample-app + pod 'Skyflow', ">= 1.23.0" + +end diff --git a/Samples/Skyflow/CollectAndRevealSample/README.md b/Skyflow/Samples/CollectAndRevealSample/README.md similarity index 100% rename from Samples/Skyflow/CollectAndRevealSample/README.md rename to Skyflow/Samples/CollectAndRevealSample/README.md diff --git a/Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.pbxproj b/Skyflow/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.pbxproj rename to Skyflow/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj diff --git a/Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/AppDelegate.swift b/Skyflow/Samples/ComposableElements/ComposableElements/AppDelegate.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/AppDelegate.swift rename to Skyflow/Samples/ComposableElements/ComposableElements/AppDelegate.swift diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json rename to Skyflow/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Skyflow/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/Contents.json b/Skyflow/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Assets.xcassets/Contents.json rename to Skyflow/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard rename to Skyflow/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/Main.storyboard b/Skyflow/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Base.lproj/Main.storyboard rename to Skyflow/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard diff --git a/Skyflow/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift b/Skyflow/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift new file mode 100644 index 00000000..0d8be466 --- /dev/null +++ b/Skyflow/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleAPICallback: Skyflow.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/ExampleTokenProvider.swift b/Skyflow/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/ExampleTokenProvider.swift rename to Skyflow/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Info.plist b/Skyflow/Samples/ComposableElements/ComposableElements/Info.plist similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Info.plist rename to Skyflow/Samples/ComposableElements/ComposableElements/Info.plist diff --git a/Skyflow/Samples/ComposableElements/ComposableElements/ResponseStructs.swift b/Skyflow/Samples/ComposableElements/ComposableElements/ResponseStructs.swift new file mode 100644 index 00000000..959044f5 --- /dev/null +++ b/Skyflow/Samples/ComposableElements/ComposableElements/ResponseStructs.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let skyflow_id: String +} diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/SceneDelegate.swift b/Skyflow/Samples/ComposableElements/ComposableElements/SceneDelegate.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/SceneDelegate.swift rename to Skyflow/Samples/ComposableElements/ComposableElements/SceneDelegate.swift diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/Untitled.swift b/Skyflow/Samples/ComposableElements/ComposableElements/Untitled.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElements/Untitled.swift rename to Skyflow/Samples/ComposableElements/ComposableElements/Untitled.swift diff --git a/Samples/Skyflow/ComposableElements/ComposableElements/ViewController.swift b/Skyflow/Samples/ComposableElements/ComposableElements/ViewController.swift similarity index 68% rename from Samples/Skyflow/ComposableElements/ComposableElements/ViewController.swift rename to Skyflow/Samples/ComposableElements/ComposableElements/ViewController.swift index 78404afe..5597e404 100644 --- a/Samples/Skyflow/ComposableElements/ComposableElements/ViewController.swift +++ b/Skyflow/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -22,7 +22,6 @@ class ViewController: UIViewController { private var revealButton: UIButton! private var revealed = false - private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -74,7 +73,7 @@ class ViewController: UIViewController { ) let collectCardNumberInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", @@ -82,7 +81,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARD_NUMBER ) let collectNameInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -90,7 +89,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARDHOLDER_NAME ) let collectCVVInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", @@ -98,7 +97,7 @@ class ViewController: UIViewController { type: .CVV ) let collectExpMonthInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -106,7 +105,7 @@ class ViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = Skyflow.CollectElementInput( - tableName: "credit_cards", + table: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -171,65 +170,26 @@ class ViewController: UIViewController { } } @objc func revealForm() { - let revealCallback = Skyflow.RevealCallback( - onSuccess: { (response: Skyflow.RevealResponse) in - for record in response.records { - if let error = record.error { - print("reveal failed:", error, "httpCode:", record.httpCode) - } else { - print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) - } - } - }, - onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } - ) - let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) - self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) + self.revealContainer?.reveal(callback: ExampleAPICallback()) } @objc func submitForm() { - let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - - // Upsert example: insert-or-update credit_cards records, matching on card_number. - // If a record with a matching card_number already exists, its fields are merged - // in (updateType: .UPDATE) instead of creating a duplicate row. - let upsertOptions = [ - Skyflow.UpsertOption(tableName: "credit_cards", uniqueColumns: ["card_number"], updateType: .UPDATE) - ] - - // additionalFields: extra records submitted alongside whatever's collected from - // the mounted elements, one entry per table. - let additionalFields = Skyflow.AdditionalFields(records: [ - Skyflow.AdditionalFieldsRecord(tableName: "credit_cards", data: ["billing_zip": "94105"]) - ]) - - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(additionalFields: additionalFields, upsert: upsertOptions)) + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: Skyflow.CollectResponse) { + internal func updateSuccess(_ response: SuccessResponse) { print(response) retryCount = 0 - for result in response.records { - if let error = result.error { - print("Record failed:", error, "httpCode:", result.httpCode) - } - } - if let tokens = response.records.first?.tokens { - updateRevealInputs(tokens: tokens) - } + updateRevealInputs(tokens: response.records[0].fields) print("Successfully got response:", response) } - internal func updateFailure(error: Skyflow.SkyflowError) { - if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Any) { + if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") - } - // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. - internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { - guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } - return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + print("Failed Operation", error) } - internal func updateRevealInputs(tokens: [String: Any]) { + internal func updateRevealInputs(tokens: Fields) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -245,48 +205,40 @@ class ViewController: UIViewController { } else { self.revealed = true } - // tokenGroupRedactions: request-level redaction per token group, applied when - // reveal() is called (see revealForm()) - not per reveal element. - self.tokenGroupRedactions = [] - func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { - if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { - self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) - } - } - addTokenGroupRedaction("REDACTED", forColumn: "card_number") - addTokenGroupRedaction("MASKED", forColumn: "cvv") - addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") - addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") - let revealCardNumberInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "card_number").token ?? "", + token: tokens.card_number, inputStyles: revealStyles, - label: "Card Number" + label: "Card Number", + redaction: .REDACTED ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "cvv").token ?? "", + token: tokens.cvv, inputStyles: revealStyles, - label: "CVV" + label: "CVV", + redaction: .MASKED ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "cardholder_name").token ?? "", + token: tokens.cardholder_name, inputStyles: revealStyles, - label: "Card Holder Name" + label: "Card Holder Name", + redaction: .DEFAULT + ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "expiry_month").token ?? "", + token: tokens.expiry_month, inputStyles: revealStyles, - label: "Expiration Month" + label: "Expiration Month", + redaction: .PLAIN_TEXT ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: self.firstToken(tokens, column: "expiry_year").token ?? "", + token: tokens.expiry_year, inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/Skyflow/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift b/Skyflow/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift rename to Skyflow/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift diff --git a/Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift b/Skyflow/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift rename to Skyflow/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift diff --git a/Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift b/Skyflow/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift similarity index 100% rename from Samples/Skyflow/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift rename to Skyflow/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift diff --git a/Skyflow/Samples/ComposableElements/Podfile b/Skyflow/Samples/ComposableElements/Podfile new file mode 100644 index 00000000..9d8d3f0e --- /dev/null +++ b/Skyflow/Samples/ComposableElements/Podfile @@ -0,0 +1,20 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'ComposableElements' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod 'AEXML', '4.6.1' + pod 'Skyflow', ">= 1.22.0" + # Pods for ComposableElements + + target 'ComposableElementsTests' do + inherit! :search_paths + # Pods for testing + end + + target 'ComposableElementsUITests' do + # Pods for testing + end + +end diff --git a/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.pbxproj b/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.pbxproj new file mode 100644 index 00000000..bdef97b9 --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.pbxproj @@ -0,0 +1,640 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */; }; + 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */; }; + 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */; }; + 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */; }; + 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */; }; + 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */; }; + 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */; }; + 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */; }; + 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */; }; + 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */; }; + 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */; }; + 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */; }; + 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 7C7BCD532A49AD6400C6FBCF /* Skyflow */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; + remoteInfo = GetSample; + }; + 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; + remoteInfo = GetSample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7C7BCD192A49A5B900C6FBCF /* GetSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GetSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 7C7BCD232A49A5B900C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7C7BCD282A49A5BA00C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleTests.swift; sourceTree = ""; }; + 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITests.swift; sourceTree = ""; }; + 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITestsLaunchTests.swift; sourceTree = ""; }; + 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; + 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; + 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7C7BCD162A49A5B900C6FBCF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C7BCD362A49A5BB00C6FBCF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7C7BCD102A49A5B900C6FBCF = { + isa = PBXGroup; + children = ( + 7C7BCD1B2A49A5B900C6FBCF /* GetSample */, + 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */, + 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */, + 7C7BCD1A2A49A5B900C6FBCF /* Products */, + ); + sourceTree = ""; + }; + 7C7BCD1A2A49A5B900C6FBCF /* Products */ = { + isa = PBXGroup; + children = ( + 7C7BCD192A49A5B900C6FBCF /* GetSample.app */, + 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */, + 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7C7BCD1B2A49A5B900C6FBCF /* GetSample */ = { + isa = PBXGroup; + children = ( + 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */, + 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */, + 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */, + 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */, + 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */, + 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */, + 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */, + 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */, + 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */, + 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */, + ); + path = GetSample; + sourceTree = ""; + }; + 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */ = { + isa = PBXGroup; + children = ( + 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */, + ); + path = GetSampleTests; + sourceTree = ""; + }; + 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */ = { + isa = PBXGroup; + children = ( + 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */, + 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */, + ); + path = GetSampleUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7C7BCD182A49A5B900C6FBCF /* GetSample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */; + buildPhases = ( + 7C7BCD152A49A5B900C6FBCF /* Sources */, + 7C7BCD162A49A5B900C6FBCF /* Frameworks */, + 7C7BCD172A49A5B900C6FBCF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GetSample; + packageProductDependencies = ( + 7C7BCD532A49AD6400C6FBCF /* Skyflow */, + ); + productName = GetSample; + productReference = 7C7BCD192A49A5B900C6FBCF /* GetSample.app */; + productType = "com.apple.product-type.application"; + }; + 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */; + buildPhases = ( + 7C7BCD2B2A49A5BB00C6FBCF /* Sources */, + 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */, + 7C7BCD2D2A49A5BB00C6FBCF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */, + ); + name = GetSampleTests; + productName = GetSampleTests; + productReference = 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */; + buildPhases = ( + 7C7BCD352A49A5BB00C6FBCF /* Sources */, + 7C7BCD362A49A5BB00C6FBCF /* Frameworks */, + 7C7BCD372A49A5BB00C6FBCF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */, + ); + name = GetSampleUITests; + productName = GetSampleUITests; + productReference = 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7C7BCD112A49A5B900C6FBCF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 7C7BCD182A49A5B900C6FBCF = { + CreatedOnToolsVersion = 14.0; + }; + 7C7BCD2E2A49A5BB00C6FBCF = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 7C7BCD182A49A5B900C6FBCF; + }; + 7C7BCD382A49A5BB00C6FBCF = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 7C7BCD182A49A5B900C6FBCF; + }; + }; + }; + buildConfigurationList = 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7C7BCD102A49A5B900C6FBCF; + packageReferences = ( + 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */, + ); + productRefGroup = 7C7BCD1A2A49A5B900C6FBCF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7C7BCD182A49A5B900C6FBCF /* GetSample */, + 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */, + 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7C7BCD172A49A5B900C6FBCF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */, + 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */, + 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C7BCD2D2A49A5BB00C6FBCF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C7BCD372A49A5BB00C6FBCF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7C7BCD152A49A5B900C6FBCF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */, + 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */, + 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */, + 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */, + 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */, + 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C7BCD2B2A49A5BB00C6FBCF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C7BCD352A49A5BB00C6FBCF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */, + 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; + targetProxy = 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */; + }; + 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; + targetProxy = 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C7BCD232A49A5B900C6FBCF /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C7BCD282A49A5BA00C6FBCF /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7C7BCD412A49A5BB00C6FBCF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7C7BCD422A49A5BB00C6FBCF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7C7BCD442A49A5BB00C6FBCF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = GetSample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7C7BCD452A49A5BB00C6FBCF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = GetSample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7C7BCD472A49A5BB00C6FBCF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; + }; + name = Debug; + }; + 7C7BCD482A49A5BB00C6FBCF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; + }; + name = Release; + }; + 7C7BCD4A2A49A5BB00C6FBCF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = GetSample; + }; + name = Debug; + }; + 7C7BCD4B2A49A5BB00C6FBCF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = GetSample; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C7BCD412A49A5BB00C6FBCF /* Debug */, + 7C7BCD422A49A5BB00C6FBCF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C7BCD442A49A5BB00C6FBCF /* Debug */, + 7C7BCD452A49A5BB00C6FBCF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C7BCD472A49A5BB00C6FBCF /* Debug */, + 7C7BCD482A49A5BB00C6FBCF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C7BCD4A2A49A5BB00C6FBCF /* Debug */, + 7C7BCD4B2A49A5BB00C6FBCF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7C7BCD532A49AD6400C6FBCF /* Skyflow */ = { + isa = XCSwiftPackageProductDependency; + package = 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */; + productName = Skyflow; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 7C7BCD112A49A5B900C6FBCF /* Project object */; +} diff --git a/Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..f5762feb --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version" : "4.6.1" + } + }, + { + "identity" : "skyflow-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/skyflowapi/skyflow-iOS.git", + "state" : { + "branch" : "main", + "revision" : "d746cff983d44dd4690780c14aa7ca60387d7850" + } + } + ], + "version" : 2 +} diff --git a/Skyflow/Samples/GetSample/GetSample/AppDelegate.swift b/Skyflow/Samples/GetSample/GetSample/AppDelegate.swift new file mode 100644 index 00000000..aec625c4 --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// GetSample +// +// Created by Bharti Sagar on 26/06/23. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json rename to Skyflow/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Skyflow/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/Contents.json b/Skyflow/Samples/GetSample/GetSample/Assets.xcassets/Contents.json similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/Assets.xcassets/Contents.json rename to Skyflow/Samples/GetSample/GetSample/Assets.xcassets/Contents.json diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard rename to Skyflow/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/Main.storyboard b/Skyflow/Samples/GetSample/GetSample/Base.lproj/Main.storyboard similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/Base.lproj/Main.storyboard rename to Skyflow/Samples/GetSample/GetSample/Base.lproj/Main.storyboard diff --git a/Skyflow/Samples/GetSample/GetSample/ExampleAPICallback.swift b/Skyflow/Samples/GetSample/GetSample/ExampleAPICallback.swift new file mode 100644 index 00000000..530d74fd --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample/ExampleAPICallback.swift @@ -0,0 +1,50 @@ +// +// ExampleAPICallback.swift +// GetSample +// +// Created by Bharti Sagar on 26/06/23. +// +import Foundation +import Skyflow + +public class ExampleAPICallback: Skyflow.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/Skyflow/Samples/GetSample/GetSample/ExampleTokenProvider.swift b/Skyflow/Samples/GetSample/GetSample/ExampleTokenProvider.swift new file mode 100644 index 00000000..75a0efee --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: Skyflow.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/Info.plist b/Skyflow/Samples/GetSample/GetSample/Info.plist similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/Info.plist rename to Skyflow/Samples/GetSample/GetSample/Info.plist diff --git a/Skyflow/Samples/GetSample/GetSample/ResponseStructs.swift b/Skyflow/Samples/GetSample/GetSample/ResponseStructs.swift new file mode 100644 index 00000000..959044f5 --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample/ResponseStructs.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let skyflow_id: String +} diff --git a/Skyflow/Samples/GetSample/GetSample/SceneDelegate.swift b/Skyflow/Samples/GetSample/GetSample/SceneDelegate.swift new file mode 100644 index 00000000..a968b5bc --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// GetSample +// +// Created by Bharti Sagar on 26/06/23. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/Skyflow/Samples/GetSample/GetSample/ViewController.swift b/Skyflow/Samples/GetSample/GetSample/ViewController.swift new file mode 100644 index 00000000..df14f26a --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSample/ViewController.swift @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import Skyflow + +class ViewController: UIViewController { + private var skyflow: Skyflow.Client? + private var stackView: UIStackView! + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + let tokenProvider = ExampleTokenProvider() + self.stackView = UIStackView() + + let config = Skyflow.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider, + options: Skyflow.Options( + logLevel: Skyflow.LogLevel.DEBUG + ) + ) + + self.skyflow = Skyflow.initialize(config) + + if self.skyflow != nil { + let getWithRedactionType = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) + getWithRedactionType.backgroundColor = .gray + getWithRedactionType.setTitle("Get Records With Redaction Type", for: .normal) + getWithRedactionType.addTarget(self, action: #selector(getRecordsWithRedactionType), for: .touchUpInside) + + let getWithGetOptionTokens = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) + getWithGetOptionTokens.backgroundColor = .gray + getWithGetOptionTokens.setTitle("Get Records with tokens", for: .normal) + getWithGetOptionTokens.addTarget(self, action: #selector(getRecordsWithGetOptionTokens), for: .touchUpInside) + + stackView.addArrangedSubview(getWithRedactionType) + stackView.addArrangedSubview(getWithGetOptionTokens) + + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.backgroundColor = .white + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 20 + ).isActive = true + scrollView.leftAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leftAnchor, + constant: 10 + ).isActive = true + scrollView.rightAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.rightAnchor, + constant: -10 + ).isActive = true + scrollView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor + ).isActive = true + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + @objc func getRecordsWithRedactionType() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + let record = ["ids": ["", ""], "table": "", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] + let recordColumn = ["columnValues": ["", ""],"table": "", "columnName": "","redaction": RedactionType.PLAIN_TEXT] as [String : Any] + + let invalidID = ["invalid skyflow ID"] + let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] + + let records = ["records": [record, recordColumn, badRecord]] + self.skyflow?.get(records: records, callback: exampleAPICallback) + } + + @objc func getRecordsWithGetOptionTokens() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + let record = ["ids": ["", ""], "table": ""] as [String : Any] + + let invalidID = ["invalid skyflow ID"] + let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] + + let records = ["records": [record, badRecord]] + self.skyflow?.get(records: records, options: GetOptions(tokens: true), callback: exampleAPICallback) + } + internal func updateSuccess(_ response: SuccessResponse) { + print(response) + print("Successfully got response:", response) + } + internal func updateFailure(error: Any) { + print("Failed Operation", error) + } +} diff --git a/Skyflow/Samples/GetSample/GetSampleTests/GetSampleTests.swift b/Skyflow/Samples/GetSample/GetSampleTests/GetSampleTests.swift new file mode 100644 index 00000000..4837e722 --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSampleTests/GetSampleTests.swift @@ -0,0 +1,36 @@ +// +// GetSampleTests.swift +// GetSampleTests +// +// Created by Bharti Sagar on 26/06/23. +// + +import XCTest +@testable import GetSample + +final class GetSampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift b/Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift new file mode 100644 index 00000000..8b26e9ab --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift @@ -0,0 +1,41 @@ +// +// GetSampleUITests.swift +// GetSampleUITests +// +// Created by Bharti Sagar on 26/06/23. +// + +import XCTest + +final class GetSampleUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift b/Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift new file mode 100644 index 00000000..75f3db9b --- /dev/null +++ b/Skyflow/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// GetSampleUITestsLaunchTests.swift +// GetSampleUITests +// +// Created by Bharti Sagar on 26/06/23. +// + +import XCTest + +final class GetSampleUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Skyflow/Samples/GetSample/Podfile b/Skyflow/Samples/GetSample/Podfile new file mode 100644 index 00000000..72b31da4 --- /dev/null +++ b/Skyflow/Samples/GetSample/Podfile @@ -0,0 +1,12 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +source 'https://github.com/CocoaPods/Specs.git' + +target 'GetSample' do + #use_frameworks! + pod 'AEXML', '4.6.1' + # Pods for GetSample + pod 'Skyflow', ">= 1.22.0" + +end diff --git a/Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.pbxproj b/Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting.xcodeproj/project.pbxproj rename to Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/AppDelegate.swift b/Skyflow/Samples/InputFormatting/InputFormatting/AppDelegate.swift similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/AppDelegate.swift rename to Skyflow/Samples/InputFormatting/InputFormatting/AppDelegate.swift diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json rename to Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json b/Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json rename to Skyflow/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard rename to Skyflow/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard b/Skyflow/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard rename to Skyflow/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard diff --git a/Skyflow/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift b/Skyflow/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift new file mode 100644 index 00000000..25f84e24 --- /dev/null +++ b/Skyflow/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Skyflow + */ +// Created by Bharti Sagar on 26/04/23. +// + +import Foundation +import Skyflow + +public class ExampleAPICallback: Skyflow.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/ExampleTokenProvider.swift b/Skyflow/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/ExampleTokenProvider.swift rename to Skyflow/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift diff --git a/Skyflow/Samples/InputFormatting/InputFormatting/Info.plist b/Skyflow/Samples/InputFormatting/InputFormatting/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/Skyflow/Samples/InputFormatting/InputFormatting/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Skyflow/Samples/InputFormatting/InputFormatting/ResponseStructs.swift b/Skyflow/Samples/InputFormatting/InputFormatting/ResponseStructs.swift new file mode 100644 index 00000000..2230b92f --- /dev/null +++ b/Skyflow/Samples/InputFormatting/InputFormatting/ResponseStructs.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// Created by Bharti Sagar on 26/04/23. +// + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let ssn: String + let phone_number: String + let license_number: String + let skyflow_id: String +} diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/SceneDelegate.swift b/Skyflow/Samples/InputFormatting/InputFormatting/SceneDelegate.swift similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormatting/SceneDelegate.swift rename to Skyflow/Samples/InputFormatting/InputFormatting/SceneDelegate.swift diff --git a/Samples/Skyflow/InputFormatting/InputFormatting/ViewController.swift b/Skyflow/Samples/InputFormatting/InputFormatting/ViewController.swift similarity index 86% rename from Samples/Skyflow/InputFormatting/InputFormatting/ViewController.swift rename to Skyflow/Samples/InputFormatting/InputFormatting/ViewController.swift index c1b5df2d..fbacc971 100644 --- a/Samples/Skyflow/InputFormatting/InputFormatting/ViewController.swift +++ b/Skyflow/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -77,7 +77,7 @@ class ViewController: UIViewController { ) let collectCardNumberInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "card_number", inputStyles: styles, label: "Card Number", @@ -85,7 +85,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARD_NUMBER ) let collectNameInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -93,7 +93,7 @@ class ViewController: UIViewController { type: Skyflow.ElementType.CARDHOLDER_NAME ) let collectCVVInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "cvv", inputStyles: styles, label: "CVV", @@ -101,7 +101,7 @@ class ViewController: UIViewController { type: .CVV ) let collectExpMonthInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -109,7 +109,7 @@ class ViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -117,7 +117,7 @@ class ViewController: UIViewController { type: .EXPIRATION_YEAR ) let collectSSNInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "ssn", inputStyles: styles, label: "SSN", @@ -125,7 +125,7 @@ class ViewController: UIViewController { type: .INPUT_FIELD ) let collectPhoneNumberInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "phone_number", inputStyles: styles, label: "Phone Number", @@ -133,7 +133,7 @@ class ViewController: UIViewController { type: .INPUT_FIELD ) let collectLicenseNumberInput = Skyflow.CollectElementInput( - tableName: "table", + table: "table", column: "license_number", inputStyles: styles, label: "License Number", @@ -201,37 +201,21 @@ class ViewController: UIViewController { } } @objc func revealForm() { - let revealCallback = Skyflow.RevealCallback( - onSuccess: { response in print("reveal success:", response) }, - onFailure: { error in print("reveal failure:", error) } - ) - self.revealContainer?.reveal(callback: revealCallback) + self.revealContainer?.reveal(callback: ExampleAPICallback()) } @objc func submitForm() { - let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: Skyflow.CollectResponse) { + internal func updateSuccess(_ response: SuccessResponse) { print(response) - for result in response.records { - if let error = result.error { - print("Record failed:", error, "httpCode:", result.httpCode) - } - } - if let tokens = response.records.first?.tokens { - updateRevealInputs(tokens: tokens) - } + updateRevealInputs(tokens: response.records[0].fields) print("Successfully got response:", response) } internal func updateFailure(error: Any) { print("Failed Operation", error) } - // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. - internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { - guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } - return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) - } - internal func updateRevealInputs(tokens: [String: Any]) { + internal func updateRevealInputs(tokens: Fields) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -248,7 +232,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "card_number").token ?? "", + token: tokens.card_number, inputStyles: revealStyles, label: "Card Number" ) @@ -257,43 +241,43 @@ class ViewController: UIViewController { options: Skyflow.RevealElementOptions(format: "XXXX-XXXX-XXXX-XXXX-XXX", translation: ["X": "[0-9]"]) ) let revealCVVtInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "cvv").token ?? "", + token: tokens.cvv, inputStyles: revealStyles, label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "cardholder_name").token ?? "", + token: tokens.cardholder_name, inputStyles: revealStyles, label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "expiry_month").token ?? "", + token: tokens.expiry_month, inputStyles: revealStyles, label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "expiry_year").token ?? "", + token: tokens.expiry_year, inputStyles: revealStyles, label: "Expiration Year" ) self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) let revealSSNInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "ssn").token ?? "", + token: tokens.ssn, inputStyles: revealStyles, label: "SSN" ) self.revealSSN = self.revealContainer?.create(input: revealSSNInput, options: Skyflow.RevealElementOptions(format: "XXX XX XXXX", translation: ["X": "[0-9]"])) let revealPhoneNumberInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "phone_number").token ?? "", + token: tokens.phone_number, inputStyles: revealStyles, label: "Phone Number" ) self.revealPhoneNumber = self.revealContainer?.create(input: revealPhoneNumberInput, options: Skyflow.RevealElementOptions(format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) let revealLicenseNumberInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "phone_number").token ?? "", + token: tokens.phone_number, inputStyles: revealStyles, label: "License Number" ) diff --git a/Samples/Skyflow/InputFormatting/InputFormattingTests/InputFormattingTests.swift b/Skyflow/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormattingTests/InputFormattingTests.swift rename to Skyflow/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift diff --git a/Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift b/Skyflow/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift rename to Skyflow/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift diff --git a/Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift b/Skyflow/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift similarity index 100% rename from Samples/Skyflow/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift rename to Skyflow/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift diff --git a/Skyflow/Samples/InputFormatting/Podfile b/Skyflow/Samples/InputFormatting/Podfile new file mode 100644 index 00000000..f4ddb57c --- /dev/null +++ b/Skyflow/Samples/InputFormatting/Podfile @@ -0,0 +1,21 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' +target 'InputFormatting' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod 'AEXML', '4.6.1' + + pod 'Skyflow', ">= 1.22.0" + + # Pods for InputFormatting + + target 'InputFormattingTests' do + inherit! :search_paths + # Pods for testing + end + + target 'InputFormattingUITests' do + # Pods for testing + end + +end diff --git a/Samples/Package.swift b/Skyflow/Samples/Package.swift similarity index 100% rename from Samples/Package.swift rename to Skyflow/Samples/Package.swift diff --git a/Skyflow/Samples/README.md b/Skyflow/Samples/README.md new file mode 100644 index 00000000..876def59 --- /dev/null +++ b/Skyflow/Samples/README.md @@ -0,0 +1,29 @@ +# Samples + +Sample apps for the Skyflow SDK (`pod 'Skyflow'` / `import Skyflow`): + +| Sample | Description | +|--------|-------------| +| [CollectAndRevealSample](CollectAndRevealSample/) | Collect card data with secure elements and reveal tokens back to values (includes card brand choice) | +| [ComposableElements](ComposableElements/) | Lay out multiple secure elements together in shared rows using a composable container | +| [GetSample](GetSample/) | Fetch records by skyflow ID or unique column values using `get()`/`getById()` | +| [InputFormatting](InputFormatting/) | Apply `format`/`translation` patterns to control how element input is displayed | +| [UpdateDataUsingElements](UpdateDataUsingElements/) | Update existing vault records through collect elements using a skyflow ID | +| [UpsertFeature](UpsertFeature/) | Insert-or-update records with upsert options on a unique column | +| [Validations](Validations/) | Built-in and custom validation rules (regex, length, element match) on collect elements | + +## Running a sample + +Please follow these below steps to run samples + +- Navigate to the desired sample +- In CMD, run the command, pod install +- Open the .xcworkspace file using xcode +- Click on Build and run + +`Note`: +In every sample, in Skyflow.Configuration(), replace with the following fields: +1. Replace the placeholder "" with the correct vaultId you want to connect +2. Replace the placeholder "" with the correct vaultURL +3. Implement the bearer token endpoint using server side auth SDK and service account file. + Replace the placeholder "" with the bearer token endpoint which gives the bearerToken, implemented at your backend. \ No newline at end of file diff --git a/Skyflow/Samples/UpdateDataUsingElements/Podfile b/Skyflow/Samples/UpdateDataUsingElements/Podfile new file mode 100644 index 00000000..288940eb --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/Podfile @@ -0,0 +1,26 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'UpdateDataUsingElements' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for UpdateDataUsingElements + pod 'AEXML', '4.6.1' + pod 'Skyflow', '1.25.0' + + target 'UpdateDataUsingElementsTests' do + inherit! :search_paths + # Pods for testing + pod 'AEXML', '4.6.1' + pod 'Skyflow', '1.25.0' + end + + target 'UpdateDataUsingElementsUITests' do + # Pods for testing + pod 'AEXML', '4.6.1' + pod 'Skyflow', '1.25.0' + + end + +end diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ae700fd2 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj @@ -0,0 +1,756 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 84209722024122879D341F43 /* Pods_UpdateDataUsingElementsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */; }; + A9BAC9FE1923AA2AF6EBDD4A /* Pods_UpdateDataUsingElements.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */; }; + F9F0ED650060535674C02713 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 8EE9B1D32F5D47F600E676C7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8EE9B1B42F5D47EF00E676C7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8EE9B1BB2F5D47EF00E676C7; + remoteInfo = UpdateDataUsingElements; + }; + 8EE9B1DD2F5D47F600E676C7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8EE9B1B42F5D47EF00E676C7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8EE9B1BB2F5D47EF00E676C7; + remoteInfo = UpdateDataUsingElements; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElementsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElementsTests.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests.release.xcconfig"; sourceTree = ""; }; + 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig"; sourceTree = ""; }; + 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements.release.xcconfig"; sourceTree = ""; }; + 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElementsTests.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests.debug.xcconfig"; sourceTree = ""; }; + 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UpdateDataUsingElements.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UpdateDataUsingElementsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UpdateDataUsingElementsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig"; sourceTree = ""; }; + DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements.debug.xcconfig"; sourceTree = ""; }; + F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 8EE9B1E42F5D47F600E676C7 /* Exceptions for "UpdateDataUsingElements" folder in "UpdateDataUsingElements" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 8EE9B1E42F5D47F600E676C7 /* Exceptions for "UpdateDataUsingElements" folder in "UpdateDataUsingElements" target */, + ); + path = UpdateDataUsingElements; + sourceTree = ""; + }; + 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = UpdateDataUsingElementsTests; + sourceTree = ""; + }; + 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = UpdateDataUsingElementsUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8EE9B1B92F5D47EF00E676C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A9BAC9FE1923AA2AF6EBDD4A /* Pods_UpdateDataUsingElements.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1CF2F5D47F600E676C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 84209722024122879D341F43 /* Pods_UpdateDataUsingElementsTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1D92F5D47F600E676C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F9F0ED650060535674C02713 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27820800536265CC8997BEDA /* Pods */ = { + isa = PBXGroup; + children = ( + DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */, + 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */, + D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */, + 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */, + 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */, + 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 46291EA2BCEB4448675D0DA8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */, + A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */, + 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8EE9B1B32F5D47EF00E676C7 = { + isa = PBXGroup; + children = ( + 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */, + 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */, + 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, + 8EE9B1BD2F5D47EF00E676C7 /* Products */, + 27820800536265CC8997BEDA /* Pods */, + 46291EA2BCEB4448675D0DA8 /* Frameworks */, + ); + sourceTree = ""; + }; + 8EE9B1BD2F5D47EF00E676C7 /* Products */ = { + isa = PBXGroup; + children = ( + 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */, + 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */, + 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8EE9B1E52F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElements" */; + buildPhases = ( + 0F8A093288F474159BEDE644 /* [CP] Check Pods Manifest.lock */, + 8EE9B1B82F5D47EF00E676C7 /* Sources */, + 8EE9B1B92F5D47EF00E676C7 /* Frameworks */, + 8EE9B1BA2F5D47EF00E676C7 /* Resources */, + E938BD775733C7F94C478863 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */, + ); + name = UpdateDataUsingElements; + productName = UpdateDataUsingElements; + productReference = 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */; + productType = "com.apple.product-type.application"; + }; + 8EE9B1D12F5D47F600E676C7 /* UpdateDataUsingElementsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8EE9B1EA2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsTests" */; + buildPhases = ( + 8B6ABD965E2972F8AC0F597B /* [CP] Check Pods Manifest.lock */, + 8EE9B1CE2F5D47F600E676C7 /* Sources */, + 8EE9B1CF2F5D47F600E676C7 /* Frameworks */, + 8EE9B1D02F5D47F600E676C7 /* Resources */, + 87992577B2A3D0F9B4A74C66 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 8EE9B1D42F5D47F600E676C7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */, + ); + name = UpdateDataUsingElementsTests; + productName = UpdateDataUsingElementsTests; + productReference = 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 8EE9B1DB2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8EE9B1ED2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsUITests" */; + buildPhases = ( + E3F917F11A21B04E3AE532A9 /* [CP] Check Pods Manifest.lock */, + 8EE9B1D82F5D47F600E676C7 /* Sources */, + 8EE9B1D92F5D47F600E676C7 /* Frameworks */, + 8EE9B1DA2F5D47F600E676C7 /* Resources */, + 7ED4115DECEAE1F715BC3D6F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 8EE9B1DE2F5D47F600E676C7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, + ); + name = UpdateDataUsingElementsUITests; + productName = UpdateDataUsingElementsUITests; + productReference = 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8EE9B1B42F5D47EF00E676C7 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 8EE9B1BB2F5D47EF00E676C7 = { + CreatedOnToolsVersion = 16.4; + }; + 8EE9B1D12F5D47F600E676C7 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 8EE9B1BB2F5D47EF00E676C7; + }; + 8EE9B1DB2F5D47F600E676C7 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 8EE9B1BB2F5D47EF00E676C7; + }; + }; + }; + buildConfigurationList = 8EE9B1B72F5D47EF00E676C7 /* Build configuration list for PBXProject "UpdateDataUsingElements" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8EE9B1B32F5D47EF00E676C7; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 8EE9B1F42F5D489500E676C7 /* XCRemoteSwiftPackageReference "skyflow-iOS" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 8EE9B1BD2F5D47EF00E676C7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */, + 8EE9B1D12F5D47F600E676C7 /* UpdateDataUsingElementsTests */, + 8EE9B1DB2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8EE9B1BA2F5D47EF00E676C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1D02F5D47F600E676C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1DA2F5D47F600E676C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0F8A093288F474159BEDE644 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElements-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7ED4115DECEAE1F715BC3D6F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 87992577B2A3D0F9B4A74C66 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8B6ABD965E2972F8AC0F597B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElementsTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E3F917F11A21B04E3AE532A9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E938BD775733C7F94C478863 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8EE9B1B82F5D47EF00E676C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1CE2F5D47F600E676C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1D82F5D47F600E676C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 8EE9B1D42F5D47F600E676C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; + targetProxy = 8EE9B1D32F5D47F600E676C7 /* PBXContainerItemProxy */; + }; + 8EE9B1DE2F5D47F600E676C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; + targetProxy = 8EE9B1DD2F5D47F600E676C7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 8EE9B1E62F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UpdateDataUsingElements/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElements; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8EE9B1E72F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UpdateDataUsingElements/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElements; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 8EE9B1E82F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8EE9B1E92F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8EE9B1EB2F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UpdateDataUsingElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UpdateDataUsingElements"; + }; + name = Debug; + }; + 8EE9B1EC2F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UpdateDataUsingElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UpdateDataUsingElements"; + }; + name = Release; + }; + 8EE9B1EE2F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UpdateDataUsingElements; + }; + name = Debug; + }; + 8EE9B1EF2F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UpdateDataUsingElements; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8EE9B1B72F5D47EF00E676C7 /* Build configuration list for PBXProject "UpdateDataUsingElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1E82F5D47F600E676C7 /* Debug */, + 8EE9B1E92F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8EE9B1E52F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1E62F5D47F600E676C7 /* Debug */, + 8EE9B1E72F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8EE9B1EA2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1EB2F5D47F600E676C7 /* Debug */, + 8EE9B1EC2F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8EE9B1ED2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1EE2F5D47F600E676C7 /* Debug */, + 8EE9B1EF2F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 8EE9B1F42F5D489500E676C7 /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.24.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + }; + rootObject = 8EE9B1B42F5D47EF00E676C7 /* Project object */; +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..28e92450 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift new file mode 100644 index 00000000..aa40b1e6 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// UpdateDataUsingElements +// +// Created by Bharti Sagar on 08/03/26. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Samples/Skyflow/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Samples/Skyflow/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json rename to Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/Skyflow/Validations/Validations/Assets.xcassets/Contents.json b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json similarity index 100% rename from Samples/Skyflow/Validations/Validations/Assets.xcassets/Contents.json rename to Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json diff --git a/Samples/Skyflow/Validations/Validations/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Samples/Skyflow/Validations/Validations/Base.lproj/LaunchScreen.storyboard rename to Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard diff --git a/Samples/Skyflow/Validations/Validations/Base.lproj/Main.storyboard b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard similarity index 100% rename from Samples/Skyflow/Validations/Validations/Base.lproj/Main.storyboard rename to Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift new file mode 100644 index 00000000..b08d2305 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleAPICallback: Skyflow.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (Any) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift new file mode 100644 index 00000000..75a0efee --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: Skyflow.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift new file mode 100644 index 00000000..4b948ce6 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields? + let table: String + let skyflow_id: String? +} + +struct Fields: Codable { + let cardholder_name: String? + let card_number: String? + let expiry_month: String? + let expiry_year: String? + let cvv: String? + let skyflow_id: String? +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift new file mode 100644 index 00000000..6f4093b7 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// UpdateDataUsingElements +// +// Created by Bharti Sagar on 08/03/26. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift new file mode 100644 index 00000000..6639ec61 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import Skyflow + +class ViewController: UIViewController { + var retryCount = 0 + private var skyflow: Skyflow.Client? + private var container: Skyflow.Container? + private var b: UIButton? + + private var stackView: UIStackView! + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + let tokenProvider = ExampleTokenProvider() + + let config = Skyflow.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider, + options: Skyflow.Options( + logLevel: Skyflow.LogLevel.DEBUG + ) + ) + + self.skyflow = Skyflow.initialize(config) + + if self.skyflow != nil { + let container = self.skyflow?.container(type: Skyflow.ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1,2,2], styles: Styles(base: Style(borderColor: UIColor.gray) ), errorTextStyles: Styles(base: Style(textColor: UIColor.red) ))) + self.container = container + self.stackView = UIStackView() + + let baseStyle = Skyflow.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + + let focusStyle = Skyflow.Style(borderColor: .blue) + + let completedStyle = Skyflow.Style(textColor: UIColor.green) + + let invalidStyle = Skyflow.Style(textColor: UIColor.red) + + let styles = Skyflow.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + + let collectCardNumberInput = Skyflow.CollectElementInput( + table: "credit_cards", + column: "card_number", + inputStyles: styles, + label: "Card Number", + placeholder: "4111-1111-1111-1111", + type: Skyflow.ElementType.CARD_NUMBER, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + + ) + let collectNameInput = Skyflow.CollectElementInput( + table: "credit_cards", + column: "cardholder_name", + inputStyles: styles, + label: "Card Holder Name", + placeholder: "John Doe", + type: Skyflow.ElementType.CARDHOLDER_NAME, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + ) + let collectCVVInput = Skyflow.CollectElementInput( + table: "credit_cards", + column: "cvv", + inputStyles: styles, + label: "CVV", + placeholder: "***", + type: .CVV, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + ) + let collectExpMonthInput = Skyflow.CollectElementInput( + table: "credit_cards", + column: "expiry_month", + inputStyles: styles, + label: "Expiration Month", + placeholder: "MM", + type: .EXPIRATION_MONTH, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + + ) + let collectExpYearInput = Skyflow.CollectElementInput( + table: "credit_cards", + column: "expiry_year", + inputStyles: styles, + label: "Expiration Year", + placeholder: "YYYY", + type: .EXPIRATION_YEAR, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + + ) + let requiredOption = Skyflow.CollectElementOptions(required: true) + let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) + let collectName = container?.create(input: collectNameInput, options: requiredOption) + let collectCVV = container?.create(input: collectCVVInput, options: requiredOption) + let collectExpMonth = container?.create(input: collectExpMonthInput, options: requiredOption) + let collectExpYear = container?.create(input: collectExpYearInput, options: requiredOption) + let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + collectButton.backgroundColor = .blue + collectButton.setTitle("Submit", for: .normal) + collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + + do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) + } catch { + print(error) + } + + stackView.addArrangedSubview(collectButton) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.backgroundColor = .white + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 20 + ).isActive = true + scrollView.leftAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leftAnchor, + constant: 10 + ).isActive = true + scrollView.rightAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.rightAnchor, + constant: -10 + ).isActive = true + scrollView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor + ).isActive = true + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + + @objc func submitForm() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + } + internal func updateSuccess(_ response: SuccessResponse) { + print(response) + retryCount = 0 + print("Successfully got response:", response) + } + internal func updateFailure(error: Any) { + if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + retryCount += 1 + submitForm() + } + print("Failed Operation", error) + } + +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift new file mode 100644 index 00000000..eada6780 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift @@ -0,0 +1,36 @@ +// +// UpdateDataUsingElementsTests.swift +// UpdateDataUsingElementsTests +// +// Created by Bharti Sagar on 08/03/26. +// + +import XCTest +@testable import UpdateDataUsingElements + +final class UpdateDataUsingElementsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift new file mode 100644 index 00000000..a921baab --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift @@ -0,0 +1,41 @@ +// +// UpdateDataUsingElementsUITests.swift +// UpdateDataUsingElementsUITests +// +// Created by Bharti Sagar on 08/03/26. +// + +import XCTest + +final class UpdateDataUsingElementsUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift new file mode 100644 index 00000000..81b9cb73 --- /dev/null +++ b/Skyflow/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// UpdateDataUsingElementsUITestsLaunchTests.swift +// UpdateDataUsingElementsUITests +// +// Created by Bharti Sagar on 08/03/26. +// + +import XCTest + +final class UpdateDataUsingElementsUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Skyflow/Samples/UpsertFeature/Podfile b/Skyflow/Samples/UpsertFeature/Podfile new file mode 100644 index 00000000..c5c4165e --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/Podfile @@ -0,0 +1,4 @@ +source 'https://github.com/CocoaPods/Specs.git' +target 'UpsertFeature' do + pod 'Skyflow', ">= 1.22.0" +end diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj rename to Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj diff --git a/Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Samples/Skyflow/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 55% rename from Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d02f09c6..040de638 100644 --- a/Samples/Skyflow/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "AEXML", + "repositoryURL": "https://github.com/tadija/AEXML.git", + "state": { + "branch": null, + "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version": "4.6.1" + } + }, { "package": "Skyflow", "repositoryURL": "https://github.com/skyflowapi/skyflow-iOS.git", diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/AppDelegate.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/AppDelegate.swift rename to Skyflow/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Contents.json b/Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Contents.json rename to Skyflow/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard b/Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/Info.plist b/Skyflow/Samples/UpsertFeature/UpsertFeature/Info.plist similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/Info.plist rename to Skyflow/Samples/UpsertFeature/UpsertFeature/Info.plist diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift new file mode 100644 index 00000000..6ea1a8a6 --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardnumber: String + let cvv: String + let skyflow_id: String +} diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/SceneDelegate.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift similarity index 100% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/SceneDelegate.swift rename to Skyflow/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift diff --git a/Samples/Skyflow/UpsertFeature/UpsertFeature/ViewController.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift similarity index 72% rename from Samples/Skyflow/UpsertFeature/UpsertFeature/ViewController.swift rename to Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift index c01a0784..86f38902 100644 --- a/Samples/Skyflow/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -15,7 +15,6 @@ class ViewController: UIViewController { private var revealCvv: Label? private var revealButton: UIButton! private var revealed = false - private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -41,7 +40,7 @@ class ViewController: UIViewController { textAlignment: .left, textColor: .blue ) - let focusStyle = Skyflow.Style(borderColor: .blue) + let focusStyle = Skyflow.Style(borderColor: .blue) ̰ let completedStyle = Skyflow.Style(borderColor: UIColor.green, textColor: UIColor.green) let invalidStyle = Skyflow.Style(borderColor: UIColor.red, textColor: UIColor.red) let styles = Skyflow.Styles( @@ -86,7 +85,7 @@ class ViewController: UIViewController { ) // keep card number as unique column for testing upsert feature let collectCardNumberInput = Skyflow.CollectElementInput( - tableName: "persons", + table: "persons", column: "cardnumber", inputStyles: styles, labelStyles: labelStyles, @@ -98,7 +97,7 @@ class ViewController: UIViewController { let requiredOption = Skyflow.CollectElementOptions(required: true) let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) let collectCvvInput = Skyflow.CollectElementInput( - tableName: "persons", + table: "persons", column: "cvv", inputStyles: styles, label: "Cvv", @@ -151,52 +150,25 @@ class ViewController: UIViewController { } @objc func revealForm() { - let revealCallback = Skyflow.RevealCallback( - onSuccess: { (response: Skyflow.RevealResponse) in - for record in response.records { - if let error = record.error { - print("reveal failed:", error, "httpCode:", record.httpCode) - } else { - print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) - } - } - }, - onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } - ) - let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) - self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) + self.revealContainer?.reveal(callback: ExampleAPICallback()) } @objc func submitForm() { - let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - let upsertOptions = [Skyflow.UpsertOption(tableName: "persons", uniqueColumns: ["cardnumber"], updateType: .UPDATE)] - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(upsert: upsertOptions)) + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] + container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: Skyflow.CollectResponse) { - print(response) - for result in response.records { - if let error = result.error { - print("Record failed:", error, "httpCode:", result.httpCode) - } - } - if let tokens = response.records.first?.tokens { - updateRevealInputs(tokens: tokens) - } + internal func updateSuccess(_ response: SuccessResponse) { + updateRevealInputs(tokens: response.records[0].fields) print("Successfully got response:", response) } - internal func updateFailure(error: Skyflow.SkyflowError) { - print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") - } - - // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. - internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { - guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } - return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + internal func updateFailure() { + print("Failed Operation") } - internal func updateRevealInputs(tokens: [String: Any]) { + internal func updateRevealInputs(tokens: Fields) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -212,30 +184,21 @@ class ViewController: UIViewController { } else { self.revealed = true } - // tokenGroupRedactions: request-level redaction per token group, applied when - // reveal() is called (see revealForm()) - not per reveal element. - self.tokenGroupRedactions = [] - func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { - if let tokenGroupName = firstToken(tokens, column: column).tokenGroupName { - self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) - } - } - addTokenGroupRedaction("DEFAULT", forColumn: "cardnumber") - addTokenGroupRedaction("DEFAULT", forColumn: "cvv") - let revealCardNumberInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "cardnumber").token, + token: tokens.cardnumber, inputStyles: revealStyles, - label: "Card Number" + label: "Card Number", + redaction: .DEFAULT ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCvvInput = Skyflow.RevealElementInput( - token: firstToken(tokens, column: "cvv").token, + token: tokens.cvv, inputStyles: revealStyles, - label: "Cvv" + label: "Cvv", + redaction: .DEFAULT ) self.revealCvv = self.revealContainer?.create( input: revealCvvInput, diff --git a/Skyflow/Samples/Validations/Podfile b/Skyflow/Samples/Validations/Podfile new file mode 100644 index 00000000..cb757a40 --- /dev/null +++ b/Skyflow/Samples/Validations/Podfile @@ -0,0 +1,12 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +source 'https://github.com/CocoaPods/Specs.git' + +target 'Validations' do + #use_frameworks! + pod 'AEXML', '4.6.1' + # Pods for skyflow-iOS-sample-app + pod 'Skyflow', ">= 1.22.0" + +end diff --git a/Samples/Skyflow/Validations/README.md b/Skyflow/Samples/Validations/README.md similarity index 100% rename from Samples/Skyflow/Validations/README.md rename to Skyflow/Samples/Validations/README.md diff --git a/Samples/Skyflow/Validations/Validations.xcodeproj/project.pbxproj b/Skyflow/Samples/Validations/Validations.xcodeproj/project.pbxproj similarity index 100% rename from Samples/Skyflow/Validations/Validations.xcodeproj/project.pbxproj rename to Skyflow/Samples/Validations/Validations.xcodeproj/project.pbxproj diff --git a/Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Skyflow/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Samples/Skyflow/Validations/Validations/AppDelegate.swift b/Skyflow/Samples/Validations/Validations/AppDelegate.swift similarity index 100% rename from Samples/Skyflow/Validations/Validations/AppDelegate.swift rename to Skyflow/Samples/Validations/Validations/AppDelegate.swift diff --git a/Skyflow/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json b/Skyflow/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Skyflow/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/Skyflow/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json b/Skyflow/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Samples/Skyflow/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Skyflow/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Skyflow/Samples/Validations/Validations/Assets.xcassets/Contents.json b/Skyflow/Samples/Validations/Validations/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Skyflow/Samples/Validations/Validations/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Skyflow/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard b/Skyflow/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Skyflow/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Skyflow/Samples/Validations/Validations/Base.lproj/Main.storyboard b/Skyflow/Samples/Validations/Validations/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/Skyflow/Samples/Validations/Validations/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/Skyflow/Validations/Validations/ExampleAPICallback.swift b/Skyflow/Samples/Validations/Validations/ExampleAPICallback.swift similarity index 100% rename from Samples/Skyflow/Validations/Validations/ExampleAPICallback.swift rename to Skyflow/Samples/Validations/Validations/ExampleAPICallback.swift diff --git a/Samples/Skyflow/Validations/Validations/ExampleTokenProvider.swift b/Skyflow/Samples/Validations/Validations/ExampleTokenProvider.swift similarity index 100% rename from Samples/Skyflow/Validations/Validations/ExampleTokenProvider.swift rename to Skyflow/Samples/Validations/Validations/ExampleTokenProvider.swift diff --git a/Samples/Skyflow/Validations/Validations/Info.plist b/Skyflow/Samples/Validations/Validations/Info.plist similarity index 100% rename from Samples/Skyflow/Validations/Validations/Info.plist rename to Skyflow/Samples/Validations/Validations/Info.plist diff --git a/Samples/Skyflow/Validations/Validations/SceneDelegate.swift b/Skyflow/Samples/Validations/Validations/SceneDelegate.swift similarity index 100% rename from Samples/Skyflow/Validations/Validations/SceneDelegate.swift rename to Skyflow/Samples/Validations/Validations/SceneDelegate.swift diff --git a/Samples/Skyflow/Validations/Validations/ViewController.swift b/Skyflow/Samples/Validations/Validations/ViewController.swift similarity index 100% rename from Samples/Skyflow/Validations/Validations/ViewController.swift rename to Skyflow/Samples/Validations/Validations/ViewController.swift diff --git a/Sources/Skyflow/Exports.swift b/Skyflow/Sources/Exports.swift similarity index 100% rename from Sources/Skyflow/Exports.swift rename to Skyflow/Sources/Exports.swift diff --git a/Sources/Skyflow/Init.swift b/Skyflow/Sources/Init.swift similarity index 100% rename from Sources/Skyflow/Init.swift rename to Skyflow/Sources/Init.swift diff --git a/Sources/Skyflow/client/ClientOperations.swift b/Skyflow/Sources/client/ClientOperations.swift similarity index 100% rename from Sources/Skyflow/client/ClientOperations.swift rename to Skyflow/Sources/client/ClientOperations.swift diff --git a/Sources/Skyflow/collect/CollectContainerOperations.swift b/Skyflow/Sources/collect/CollectContainerOperations.swift similarity index 100% rename from Sources/Skyflow/collect/CollectContainerOperations.swift rename to Skyflow/Sources/collect/CollectContainerOperations.swift diff --git a/Sources/Skyflow/collect/CollectRequestBody.swift b/Skyflow/Sources/collect/CollectRequestBody.swift similarity index 100% rename from Sources/Skyflow/collect/CollectRequestBody.swift rename to Skyflow/Sources/collect/CollectRequestBody.swift diff --git a/Sources/Skyflow/collect/ICOptions.swift b/Skyflow/Sources/collect/ICOptions.swift similarity index 100% rename from Sources/Skyflow/collect/ICOptions.swift rename to Skyflow/Sources/collect/ICOptions.swift diff --git a/Sources/Skyflow/composable/ComposableContainerOperations.swift b/Skyflow/Sources/composable/ComposableContainerOperations.swift similarity index 100% rename from Sources/Skyflow/composable/ComposableContainerOperations.swift rename to Skyflow/Sources/composable/ComposableContainerOperations.swift diff --git a/Sources/Skyflow/models/CollectElementInput.swift b/Skyflow/Sources/models/CollectElementInput.swift similarity index 100% rename from Sources/Skyflow/models/CollectElementInput.swift rename to Skyflow/Sources/models/CollectElementInput.swift diff --git a/Sources/Skyflow/models/CollectElementOptions.swift b/Skyflow/Sources/models/CollectElementOptions.swift similarity index 100% rename from Sources/Skyflow/models/CollectElementOptions.swift rename to Skyflow/Sources/models/CollectElementOptions.swift diff --git a/Sources/Skyflow/models/CollectOptions.swift b/Skyflow/Sources/models/CollectOptions.swift similarity index 100% rename from Sources/Skyflow/models/CollectOptions.swift rename to Skyflow/Sources/models/CollectOptions.swift diff --git a/Sources/Skyflow/models/Configuration.swift b/Skyflow/Sources/models/Configuration.swift similarity index 100% rename from Sources/Skyflow/models/Configuration.swift rename to Skyflow/Sources/models/Configuration.swift diff --git a/Sources/Skyflow/models/ContainerOptions.swift b/Skyflow/Sources/models/ContainerOptions.swift similarity index 100% rename from Sources/Skyflow/models/ContainerOptions.swift rename to Skyflow/Sources/models/ContainerOptions.swift diff --git a/Sources/Skyflow/models/GetByIdRecord.swift b/Skyflow/Sources/models/GetByIdRecord.swift similarity index 100% rename from Sources/Skyflow/models/GetByIdRecord.swift rename to Skyflow/Sources/models/GetByIdRecord.swift diff --git a/Sources/Skyflow/models/GetOptions.swift b/Skyflow/Sources/models/GetOptions.swift similarity index 100% rename from Sources/Skyflow/models/GetOptions.swift rename to Skyflow/Sources/models/GetOptions.swift diff --git a/Sources/Skyflow/models/GetRecord.swift b/Skyflow/Sources/models/GetRecord.swift similarity index 100% rename from Sources/Skyflow/models/GetRecord.swift rename to Skyflow/Sources/models/GetRecord.swift diff --git a/Sources/Skyflow/models/InsertOptions.swift b/Skyflow/Sources/models/InsertOptions.swift similarity index 100% rename from Sources/Skyflow/models/InsertOptions.swift rename to Skyflow/Sources/models/InsertOptions.swift diff --git a/Sources/Skyflow/models/RevealElementInput.swift b/Skyflow/Sources/models/RevealElementInput.swift similarity index 100% rename from Sources/Skyflow/models/RevealElementInput.swift rename to Skyflow/Sources/models/RevealElementInput.swift diff --git a/Sources/Skyflow/models/RevealElementOptions.swift b/Skyflow/Sources/models/RevealElementOptions.swift similarity index 100% rename from Sources/Skyflow/models/RevealElementOptions.swift rename to Skyflow/Sources/models/RevealElementOptions.swift diff --git a/Sources/Skyflow/models/RevealOptions.swift b/Skyflow/Sources/models/RevealOptions.swift similarity index 100% rename from Sources/Skyflow/models/RevealOptions.swift rename to Skyflow/Sources/models/RevealOptions.swift diff --git a/Sources/Skyflow/networking/APIClientLegacy.swift b/Skyflow/Sources/networking/APIClientLegacy.swift similarity index 100% rename from Sources/Skyflow/networking/APIClientLegacy.swift rename to Skyflow/Sources/networking/APIClientLegacy.swift diff --git a/Sources/Skyflow/networking/CollectAPICallback.swift b/Skyflow/Sources/networking/CollectAPICallback.swift similarity index 100% rename from Sources/Skyflow/networking/CollectAPICallback.swift rename to Skyflow/Sources/networking/CollectAPICallback.swift diff --git a/Sources/Skyflow/networking/GetAPICallback.swift b/Skyflow/Sources/networking/GetAPICallback.swift similarity index 100% rename from Sources/Skyflow/networking/GetAPICallback.swift rename to Skyflow/Sources/networking/GetAPICallback.swift diff --git a/Sources/Skyflow/networking/InsertAPICallback.swift b/Skyflow/Sources/networking/InsertAPICallback.swift similarity index 100% rename from Sources/Skyflow/networking/InsertAPICallback.swift rename to Skyflow/Sources/networking/InsertAPICallback.swift diff --git a/Sources/Skyflow/networking/RevealApiCallback.swift b/Skyflow/Sources/networking/RevealApiCallback.swift similarity index 100% rename from Sources/Skyflow/networking/RevealApiCallback.swift rename to Skyflow/Sources/networking/RevealApiCallback.swift diff --git a/Sources/Skyflow/networking/RevealByIDAPICallback.swift b/Skyflow/Sources/networking/RevealByIDAPICallback.swift similarity index 100% rename from Sources/Skyflow/networking/RevealByIDAPICallback.swift rename to Skyflow/Sources/networking/RevealByIDAPICallback.swift diff --git a/Sources/Skyflow/networking/RevealRequestRecord.swift b/Skyflow/Sources/networking/RevealRequestRecord.swift similarity index 100% rename from Sources/Skyflow/networking/RevealRequestRecord.swift rename to Skyflow/Sources/networking/RevealRequestRecord.swift diff --git a/Sources/Skyflow/reveal/RevealContainerOperations.swift b/Skyflow/Sources/reveal/RevealContainerOperations.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealContainerOperations.swift rename to Skyflow/Sources/reveal/RevealContainerOperations.swift diff --git a/Sources/Skyflow/reveal/RevealRequestBody.swift b/Skyflow/Sources/reveal/RevealRequestBody.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealRequestBody.swift rename to Skyflow/Sources/reveal/RevealRequestBody.swift diff --git a/Sources/Skyflow/reveal/RevealValueCallback.swift b/Skyflow/Sources/reveal/RevealValueCallback.swift similarity index 100% rename from Sources/Skyflow/reveal/RevealValueCallback.swift rename to Skyflow/Sources/reveal/RevealValueCallback.swift diff --git a/Sources/Skyflow/validations/RequestValidators.swift b/Skyflow/Sources/validations/RequestValidators.swift similarity index 100% rename from Sources/Skyflow/validations/RequestValidators.swift rename to Skyflow/Sources/validations/RequestValidators.swift diff --git a/Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift b/Skyflow/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getByIdTests/DemoImpl.swift rename to Skyflow/Tests/skyflow-iOS-getByIdTests/DemoImpl.swift diff --git a/Tests/Skyflow/skyflow-iOS-getByIdTests/XCTestManifests.swift b/Skyflow/Tests/skyflow-iOS-getByIdTests/XCTestManifests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getByIdTests/XCTestManifests.swift rename to Skyflow/Tests/skyflow-iOS-getByIdTests/XCTestManifests.swift diff --git a/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift b/Skyflow/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift rename to Skyflow/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift b/Skyflow/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift rename to Skyflow/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdUtilTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift b/Skyflow/Tests/skyflow-iOS-getTests/DemoImpl.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getTests/DemoImpl.swift rename to Skyflow/Tests/skyflow-iOS-getTests/DemoImpl.swift diff --git a/Tests/Skyflow/skyflow-iOS-getTests/XCTestManifests.swift b/Skyflow/Tests/skyflow-iOS-getTests/XCTestManifests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getTests/XCTestManifests.swift rename to Skyflow/Tests/skyflow-iOS-getTests/XCTestManifests.swift diff --git a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift b/Skyflow/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getTests.swift rename to Skyflow/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift b/Skyflow/Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift rename to Skyflow/Tests/skyflow-iOS-getTests/skyflow_iOS_getUtilTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift b/Skyflow/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift rename to Skyflow/Tests/skyflow-iOS-getTests/skyflow_iOS_validateGetRecordTest.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/CoreRequestValidatorsTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/DemoImpl.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/DemoImpl.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/DemoImpl.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/DemoImpl.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/MockURLProtocol.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/MockURLProtocol.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/MockURLProtocol.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectApiCallbackTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_dormantCollectRequestBodyTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyClientApiTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectApiCallbackCoverageTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyCollectOpsTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyRevealOpsTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyUtilTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_legacyValidatorTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift b/Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift rename to Skyflow/Tests/skyflow-iOS-legacyTests/skyflow_iOS_revealApiCallbackTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/ClientScenario.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/ClientScenario.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/DemoImpl.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/DetokenizeScenario.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/DetokenizeScenario.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/DetokenizeScenario.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/GetByIdScenario.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/GetByIdScenario.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/GetByIdScenario.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/InsertScenario.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/InsertScenario.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/InsertScenario.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/TestData.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/TestData.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/TestData.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/XCTestManifests.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/XCTestManifests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/XCTestManifests.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/XCTestManifests.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_detokenizeScenarioTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_getByIdScenarioTests.swift diff --git a/Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift b/Skyflow/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift similarity index 100% rename from Tests/Skyflow/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift rename to Skyflow/Tests/skyflow-iOS-scenarioTests/skyflow_iOS_insertScenarioTests.swift diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Amex-Card.imageset/Amex-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Amex-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Cartes-Bancaires-Card.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Cartes-Bancaires-Card.imageset/Contents.json diff --git a/SkyflowCore/Sources/Resources/Assets.xcassets/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowCore/Sources/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Copy-Icon.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Copy-Icon.imageset/Copy-Icon.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Diners-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Diners-Card.imageset/Diners-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Discover-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Discover-Card.imageset/Discover-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Hipercard-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Hipercard-Card.imageset/Hipercard-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/JCB-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/JCB-Card.imageset/JCB-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Maestro-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Maestro-Card.imageset/Maestro-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Mastercard-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Mastercard-Card.imageset/Mastercard-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Success-Icon.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Success-Icon.imageset/Success-Icon.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Unionpay-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Unionpay-Card.imageset/Unionpay-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Unknown-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Unknown-Card.imageset/Unknown-Card@3x.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/Visa-Card.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png b/SkyflowCore/Sources/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/Visa-Card.imageset/Group 82.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/checkmark.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/checkmark.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/checkmark.png b/SkyflowCore/Sources/Resources/Assets.xcassets/checkmark.imageset/checkmark.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/checkmark.imageset/checkmark.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/checkmark.imageset/checkmark.png diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/Contents.json b/SkyflowCore/Sources/Resources/Assets.xcassets/dropdown.imageset/Contents.json similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/Contents.json rename to SkyflowCore/Sources/Resources/Assets.xcassets/dropdown.imageset/Contents.json diff --git a/Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/dropdown.png b/SkyflowCore/Sources/Resources/Assets.xcassets/dropdown.imageset/dropdown.png similarity index 100% rename from Sources/SkyflowCore/Resources/Assets.xcassets/dropdown.imageset/dropdown.png rename to SkyflowCore/Sources/Resources/Assets.xcassets/dropdown.imageset/dropdown.png diff --git a/Sources/SkyflowCore/Version.swift b/SkyflowCore/Sources/Version.swift similarity index 100% rename from Sources/SkyflowCore/Version.swift rename to SkyflowCore/Sources/Version.swift diff --git a/Sources/SkyflowCore/client/Client.swift b/SkyflowCore/Sources/client/Client.swift similarity index 100% rename from Sources/SkyflowCore/client/Client.swift rename to SkyflowCore/Sources/client/Client.swift diff --git a/Sources/SkyflowCore/client/Env.swift b/SkyflowCore/Sources/client/Env.swift similarity index 100% rename from Sources/SkyflowCore/client/Env.swift rename to SkyflowCore/Sources/client/Env.swift diff --git a/Sources/SkyflowCore/client/Options.swift b/SkyflowCore/Sources/client/Options.swift similarity index 100% rename from Sources/SkyflowCore/client/Options.swift rename to SkyflowCore/Sources/client/Options.swift diff --git a/Sources/SkyflowCore/collect/CollectContainer.swift b/SkyflowCore/Sources/collect/CollectContainer.swift similarity index 100% rename from Sources/SkyflowCore/collect/CollectContainer.swift rename to SkyflowCore/Sources/collect/CollectContainer.swift diff --git a/Sources/SkyflowCore/collect/elements/BaseElement.swift b/SkyflowCore/Sources/collect/elements/BaseElement.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/BaseElement.swift rename to SkyflowCore/Sources/collect/elements/BaseElement.swift diff --git a/Sources/SkyflowCore/collect/elements/Element.swift b/SkyflowCore/Sources/collect/elements/Element.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/Element.swift rename to SkyflowCore/Sources/collect/elements/Element.swift diff --git a/Sources/SkyflowCore/collect/elements/ElementType.swift b/SkyflowCore/Sources/collect/elements/ElementType.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/ElementType.swift rename to SkyflowCore/Sources/collect/elements/ElementType.swift diff --git a/Sources/SkyflowCore/collect/elements/FormatTextField.swift b/SkyflowCore/Sources/collect/elements/FormatTextField.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/FormatTextField.swift rename to SkyflowCore/Sources/collect/elements/FormatTextField.swift diff --git a/Sources/SkyflowCore/collect/elements/PaddingLabel.swift b/SkyflowCore/Sources/collect/elements/PaddingLabel.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/PaddingLabel.swift rename to SkyflowCore/Sources/collect/elements/PaddingLabel.swift diff --git a/Sources/SkyflowCore/collect/elements/SkyflowElement.swift b/SkyflowCore/Sources/collect/elements/SkyflowElement.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/SkyflowElement.swift rename to SkyflowCore/Sources/collect/elements/SkyflowElement.swift diff --git a/Sources/SkyflowCore/collect/elements/TextField.swift b/SkyflowCore/Sources/collect/elements/TextField.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/TextField.swift rename to SkyflowCore/Sources/collect/elements/TextField.swift diff --git a/Sources/SkyflowCore/collect/elements/TextFieldValidationDelegate.swift b/SkyflowCore/Sources/collect/elements/TextFieldValidationDelegate.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/TextFieldValidationDelegate.swift rename to SkyflowCore/Sources/collect/elements/TextFieldValidationDelegate.swift diff --git a/Sources/SkyflowCore/collect/elements/state/State.swift b/SkyflowCore/Sources/collect/elements/state/State.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/state/State.swift rename to SkyflowCore/Sources/collect/elements/state/State.swift diff --git a/Sources/SkyflowCore/collect/elements/state/StateforText.swift b/SkyflowCore/Sources/collect/elements/state/StateforText.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/state/StateforText.swift rename to SkyflowCore/Sources/collect/elements/state/StateforText.swift diff --git a/Sources/SkyflowCore/collect/elements/styles/Style.swift b/SkyflowCore/Sources/collect/elements/styles/Style.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/styles/Style.swift rename to SkyflowCore/Sources/collect/elements/styles/Style.swift diff --git a/Sources/SkyflowCore/collect/elements/styles/Styles.swift b/SkyflowCore/Sources/collect/elements/styles/Styles.swift similarity index 100% rename from Sources/SkyflowCore/collect/elements/styles/Styles.swift rename to SkyflowCore/Sources/collect/elements/styles/Styles.swift diff --git a/Sources/SkyflowCore/composable/BaseContainerOptions.swift b/SkyflowCore/Sources/composable/BaseContainerOptions.swift similarity index 100% rename from Sources/SkyflowCore/composable/BaseContainerOptions.swift rename to SkyflowCore/Sources/composable/BaseContainerOptions.swift diff --git a/Sources/SkyflowCore/composable/ComposableContainer.swift b/SkyflowCore/Sources/composable/ComposableContainer.swift similarity index 100% rename from Sources/SkyflowCore/composable/ComposableContainer.swift rename to SkyflowCore/Sources/composable/ComposableContainer.swift diff --git a/Sources/SkyflowCore/containers/Container.swift b/SkyflowCore/Sources/containers/Container.swift similarity index 100% rename from Sources/SkyflowCore/containers/Container.swift rename to SkyflowCore/Sources/containers/Container.swift diff --git a/Sources/SkyflowCore/containers/ContainerProtocol.swift b/SkyflowCore/Sources/containers/ContainerProtocol.swift similarity index 100% rename from Sources/SkyflowCore/containers/ContainerProtocol.swift rename to SkyflowCore/Sources/containers/ContainerProtocol.swift diff --git a/Sources/SkyflowCore/containers/ContainerType.swift b/SkyflowCore/Sources/containers/ContainerType.swift similarity index 100% rename from Sources/SkyflowCore/containers/ContainerType.swift rename to SkyflowCore/Sources/containers/ContainerType.swift diff --git a/Sources/SkyflowCore/errors/ErrorCodes.swift b/SkyflowCore/Sources/errors/ErrorCodes.swift similarity index 100% rename from Sources/SkyflowCore/errors/ErrorCodes.swift rename to SkyflowCore/Sources/errors/ErrorCodes.swift diff --git a/Sources/SkyflowCore/logging/ContextOptions.swift b/SkyflowCore/Sources/logging/ContextOptions.swift similarity index 100% rename from Sources/SkyflowCore/logging/ContextOptions.swift rename to SkyflowCore/Sources/logging/ContextOptions.swift diff --git a/Sources/SkyflowCore/logging/EventName.swift b/SkyflowCore/Sources/logging/EventName.swift similarity index 100% rename from Sources/SkyflowCore/logging/EventName.swift rename to SkyflowCore/Sources/logging/EventName.swift diff --git a/Sources/SkyflowCore/logging/InterfaceName.swift b/SkyflowCore/Sources/logging/InterfaceName.swift similarity index 100% rename from Sources/SkyflowCore/logging/InterfaceName.swift rename to SkyflowCore/Sources/logging/InterfaceName.swift diff --git a/Sources/SkyflowCore/logging/Log.swift b/SkyflowCore/Sources/logging/Log.swift similarity index 100% rename from Sources/SkyflowCore/logging/Log.swift rename to SkyflowCore/Sources/logging/Log.swift diff --git a/Sources/SkyflowCore/logging/LogLevel.swift b/SkyflowCore/Sources/logging/LogLevel.swift similarity index 100% rename from Sources/SkyflowCore/logging/LogLevel.swift rename to SkyflowCore/Sources/logging/LogLevel.swift diff --git a/Sources/SkyflowCore/logging/Message.swift b/SkyflowCore/Sources/logging/Message.swift similarity index 100% rename from Sources/SkyflowCore/logging/Message.swift rename to SkyflowCore/Sources/logging/Message.swift diff --git a/Sources/SkyflowCore/models/AutoshiftAllowed.swift b/SkyflowCore/Sources/models/AutoshiftAllowed.swift similarity index 100% rename from Sources/SkyflowCore/models/AutoshiftAllowed.swift rename to SkyflowCore/Sources/models/AutoshiftAllowed.swift diff --git a/Sources/SkyflowCore/models/BaseCollectElementInput.swift b/SkyflowCore/Sources/models/BaseCollectElementInput.swift similarity index 100% rename from Sources/SkyflowCore/models/BaseCollectElementInput.swift rename to SkyflowCore/Sources/models/BaseCollectElementInput.swift diff --git a/Sources/SkyflowCore/models/BaseCollectElementOptions.swift b/SkyflowCore/Sources/models/BaseCollectElementOptions.swift similarity index 100% rename from Sources/SkyflowCore/models/BaseCollectElementOptions.swift rename to SkyflowCore/Sources/models/BaseCollectElementOptions.swift diff --git a/Sources/SkyflowCore/models/BaseConfiguration.swift b/SkyflowCore/Sources/models/BaseConfiguration.swift similarity index 100% rename from Sources/SkyflowCore/models/BaseConfiguration.swift rename to SkyflowCore/Sources/models/BaseConfiguration.swift diff --git a/Sources/SkyflowCore/models/BaseRevealElementInput.swift b/SkyflowCore/Sources/models/BaseRevealElementInput.swift similarity index 100% rename from Sources/SkyflowCore/models/BaseRevealElementInput.swift rename to SkyflowCore/Sources/models/BaseRevealElementInput.swift diff --git a/Sources/SkyflowCore/models/BaseRevealElementOptions.swift b/SkyflowCore/Sources/models/BaseRevealElementOptions.swift similarity index 100% rename from Sources/SkyflowCore/models/BaseRevealElementOptions.swift rename to SkyflowCore/Sources/models/BaseRevealElementOptions.swift diff --git a/Sources/SkyflowCore/models/RedactionType.swift b/SkyflowCore/Sources/models/RedactionType.swift similarity index 100% rename from Sources/SkyflowCore/models/RedactionType.swift rename to SkyflowCore/Sources/models/RedactionType.swift diff --git a/Sources/SkyflowCore/networking/APIClient.swift b/SkyflowCore/Sources/networking/APIClient.swift similarity index 100% rename from Sources/SkyflowCore/networking/APIClient.swift rename to SkyflowCore/Sources/networking/APIClient.swift diff --git a/Sources/SkyflowCore/networking/TokenAPICallback.swift b/SkyflowCore/Sources/networking/TokenAPICallback.swift similarity index 100% rename from Sources/SkyflowCore/networking/TokenAPICallback.swift rename to SkyflowCore/Sources/networking/TokenAPICallback.swift diff --git a/Sources/SkyflowCore/protocols/Callback.swift b/SkyflowCore/Sources/protocols/Callback.swift similarity index 100% rename from Sources/SkyflowCore/protocols/Callback.swift rename to SkyflowCore/Sources/protocols/Callback.swift diff --git a/Sources/SkyflowCore/protocols/TokenProvider.swift b/SkyflowCore/Sources/protocols/TokenProvider.swift similarity index 100% rename from Sources/SkyflowCore/protocols/TokenProvider.swift rename to SkyflowCore/Sources/protocols/TokenProvider.swift diff --git a/Sources/SkyflowCore/reveal/FormatLabel.swift b/SkyflowCore/Sources/reveal/FormatLabel.swift similarity index 100% rename from Sources/SkyflowCore/reveal/FormatLabel.swift rename to SkyflowCore/Sources/reveal/FormatLabel.swift diff --git a/Sources/SkyflowCore/reveal/Label.swift b/SkyflowCore/Sources/reveal/Label.swift similarity index 100% rename from Sources/SkyflowCore/reveal/Label.swift rename to SkyflowCore/Sources/reveal/Label.swift diff --git a/Sources/SkyflowCore/reveal/RevealContainer.swift b/SkyflowCore/Sources/reveal/RevealContainer.swift similarity index 100% rename from Sources/SkyflowCore/reveal/RevealContainer.swift rename to SkyflowCore/Sources/reveal/RevealContainer.swift diff --git a/Sources/SkyflowCore/reveal/RevealErrorRecord.swift b/SkyflowCore/Sources/reveal/RevealErrorRecord.swift similarity index 100% rename from Sources/SkyflowCore/reveal/RevealErrorRecord.swift rename to SkyflowCore/Sources/reveal/RevealErrorRecord.swift diff --git a/Sources/SkyflowCore/reveal/RevealSuccessRecord.swift b/SkyflowCore/Sources/reveal/RevealSuccessRecord.swift similarity index 100% rename from Sources/SkyflowCore/reveal/RevealSuccessRecord.swift rename to SkyflowCore/Sources/reveal/RevealSuccessRecord.swift diff --git a/Sources/SkyflowCore/reveal/SkyflowLabelView.swift b/SkyflowCore/Sources/reveal/SkyflowLabelView.swift similarity index 100% rename from Sources/SkyflowCore/reveal/SkyflowLabelView.swift rename to SkyflowCore/Sources/reveal/SkyflowLabelView.swift diff --git a/Sources/SkyflowCore/reveal/UILabel+Copyable.swift b/SkyflowCore/Sources/reveal/UILabel+Copyable.swift similarity index 100% rename from Sources/SkyflowCore/reveal/UILabel+Copyable.swift rename to SkyflowCore/Sources/reveal/UILabel+Copyable.swift diff --git a/Sources/SkyflowCore/utils/CardBin.swift b/SkyflowCore/Sources/utils/CardBin.swift similarity index 100% rename from Sources/SkyflowCore/utils/CardBin.swift rename to SkyflowCore/Sources/utils/CardBin.swift diff --git a/Sources/SkyflowCore/utils/CardType.swift b/SkyflowCore/Sources/utils/CardType.swift similarity index 100% rename from Sources/SkyflowCore/utils/CardType.swift rename to SkyflowCore/Sources/utils/CardType.swift diff --git a/Sources/SkyflowCore/utils/CharacterSet.swift b/SkyflowCore/Sources/utils/CharacterSet.swift similarity index 100% rename from Sources/SkyflowCore/utils/CharacterSet.swift rename to SkyflowCore/Sources/utils/CharacterSet.swift diff --git a/Sources/SkyflowCore/utils/ConversionHelpers.swift b/SkyflowCore/Sources/utils/ConversionHelpers.swift similarity index 100% rename from Sources/SkyflowCore/utils/ConversionHelpers.swift rename to SkyflowCore/Sources/utils/ConversionHelpers.swift diff --git a/Sources/SkyflowCore/utils/DictionaryExtension.swift b/SkyflowCore/Sources/utils/DictionaryExtension.swift similarity index 100% rename from Sources/SkyflowCore/utils/DictionaryExtension.swift rename to SkyflowCore/Sources/utils/DictionaryExtension.swift diff --git a/Sources/SkyflowCore/utils/FetchMetrices.swift b/SkyflowCore/Sources/utils/FetchMetrices.swift similarity index 100% rename from Sources/SkyflowCore/utils/FetchMetrices.swift rename to SkyflowCore/Sources/utils/FetchMetrices.swift diff --git a/Sources/SkyflowCore/utils/SkyflowAssetBundle.swift b/SkyflowCore/Sources/utils/SkyflowAssetBundle.swift similarity index 100% rename from Sources/SkyflowCore/utils/SkyflowAssetBundle.swift rename to SkyflowCore/Sources/utils/SkyflowAssetBundle.swift diff --git a/Sources/SkyflowCore/utils/StringExtension.swift b/SkyflowCore/Sources/utils/StringExtension.swift similarity index 100% rename from Sources/SkyflowCore/utils/StringExtension.swift rename to SkyflowCore/Sources/utils/StringExtension.swift diff --git a/Sources/SkyflowCore/validations/CoreRequestValidators.swift b/SkyflowCore/Sources/validations/CoreRequestValidators.swift similarity index 100% rename from Sources/SkyflowCore/validations/CoreRequestValidators.swift rename to SkyflowCore/Sources/validations/CoreRequestValidators.swift diff --git a/Sources/SkyflowCore/validations/ElementValueMatchRule.swift b/SkyflowCore/Sources/validations/ElementValueMatchRule.swift similarity index 100% rename from Sources/SkyflowCore/validations/ElementValueMatchRule.swift rename to SkyflowCore/Sources/validations/ElementValueMatchRule.swift diff --git a/Sources/SkyflowCore/validations/LengthMatchRule.swift b/SkyflowCore/Sources/validations/LengthMatchRule.swift similarity index 100% rename from Sources/SkyflowCore/validations/LengthMatchRule.swift rename to SkyflowCore/Sources/validations/LengthMatchRule.swift diff --git a/Sources/SkyflowCore/validations/RegexMatchRule.swift b/SkyflowCore/Sources/validations/RegexMatchRule.swift similarity index 100% rename from Sources/SkyflowCore/validations/RegexMatchRule.swift rename to SkyflowCore/Sources/validations/RegexMatchRule.swift diff --git a/Sources/SkyflowCore/validations/SkyflowInternalValidatorProtocol.swift b/SkyflowCore/Sources/validations/SkyflowInternalValidatorProtocol.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowInternalValidatorProtocol.swift rename to SkyflowCore/Sources/validations/SkyflowInternalValidatorProtocol.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidateCardExpirationDate.swift b/SkyflowCore/Sources/validations/SkyflowValidateCardExpirationDate.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidateCardExpirationDate.swift rename to SkyflowCore/Sources/validations/SkyflowValidateCardExpirationDate.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidateCardNumber.swift b/SkyflowCore/Sources/validations/SkyflowValidateCardNumber.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidateCardNumber.swift rename to SkyflowCore/Sources/validations/SkyflowValidateCardNumber.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidateExpirationMonth.swift b/SkyflowCore/Sources/validations/SkyflowValidateExpirationMonth.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidateExpirationMonth.swift rename to SkyflowCore/Sources/validations/SkyflowValidateExpirationMonth.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidateExpirationYear.swift b/SkyflowCore/Sources/validations/SkyflowValidateExpirationYear.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidateExpirationYear.swift rename to SkyflowCore/Sources/validations/SkyflowValidateExpirationYear.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidateLengthMatch.swift b/SkyflowCore/Sources/validations/SkyflowValidateLengthMatch.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidateLengthMatch.swift rename to SkyflowCore/Sources/validations/SkyflowValidateLengthMatch.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidationError.swift b/SkyflowCore/Sources/validations/SkyflowValidationError.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidationError.swift rename to SkyflowCore/Sources/validations/SkyflowValidationError.swift diff --git a/Sources/SkyflowCore/validations/SkyflowValidator.swift b/SkyflowCore/Sources/validations/SkyflowValidator.swift similarity index 100% rename from Sources/SkyflowCore/validations/SkyflowValidator.swift rename to SkyflowCore/Sources/validations/SkyflowValidator.swift diff --git a/Sources/SkyflowCore/validations/ValidationRule.swift b/SkyflowCore/Sources/validations/ValidationRule.swift similarity index 100% rename from Sources/SkyflowCore/validations/ValidationRule.swift rename to SkyflowCore/Sources/validations/ValidationRule.swift diff --git a/Sources/SkyflowCore/validations/ValidationSet.swift b/SkyflowCore/Sources/validations/ValidationSet.swift similarity index 100% rename from Sources/SkyflowCore/validations/ValidationSet.swift rename to SkyflowCore/Sources/validations/ValidationSet.swift diff --git a/SkyflowFlowVault.podspec b/SkyflowFlowVault.podspec index 4bafe7d0..c69c188e 100644 --- a/SkyflowFlowVault.podspec +++ b/SkyflowFlowVault.podspec @@ -25,9 +25,9 @@ Pod::Spec.new do |spec| # CocoaPods compiles the shared core and the FlowVault contract layer into a # single module, so the SPM-only re-export shim is excluded. - spec.source_files = "Sources/SkyflowCore/**/*.{swift}", "Sources/SkyflowFlowVault/**/*.{swift}" + spec.source_files = "SkyflowCore/Sources/**/*.{swift}", "SkyflowFlowVault/Sources/**/*.{swift}" - spec.exclude_files = "Sources/SkyflowFlowVault/Exports.swift" + spec.exclude_files = "SkyflowFlowVault/Sources/Exports.swift" # `package` access-level declarations (shared across the SPM targets) require a # package name when compiled outside SPM. @@ -37,6 +37,6 @@ Pod::Spec.new do |spec| # load "Skyflow.bundle" (see SkyflowAssetBundle), and keeping one name for both # pods avoids any build-configuration dependence. The bundle is internal to the # pod and never seen by SDK consumers. - spec.resource_bundles = {'Skyflow' => ['Sources/SkyflowCore/Resources/**/*.{xcassets}'] } + spec.resource_bundles = {'Skyflow' => ['SkyflowCore/Sources/Resources/**/*.{xcassets}'] } end diff --git a/SkyflowFlowVault/README.md b/SkyflowFlowVault/README.md new file mode 100644 index 00000000..5dbec216 --- /dev/null +++ b/SkyflowFlowVault/README.md @@ -0,0 +1,2219 @@ +# SkyflowFlowVault iOS SDK +--- +Skyflow's iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. + +[![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-ios/actions) +[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-ios.svg)](https://github.com/skyflowapi/skyflow-ios/releases) +[![License](https://img.shields.io/github/license/skyflowapi/skyflow-ios)](https://github.com/skyflowapi/skyflow-ios/blob/main/LICENSE) + +# Table of Contents +- [Upgrading from PDB to FlowDB](#upgrading-from-pdb-to-flowdb) +- [Quick Start](#quick-start) + - [Collect data](#collect-data) + - [Reveal data](#reveal-data) +- [Installation](#installation) + - [Requirements](#requirements) + - [Configuration](#configuration) +- [Initializing Skyflow-iOS](#initializing-skyflow-ios) +- [Securely collecting data client-side](#securely-collecting-data-client-side) + - [Using Skyflow Elements to collect data](#using-skyflow-elements-to-collect-data) + - [Using Skyflow Elements to update data](#using-skyflow-elements-to-update-data) + - [Validations](#validations) + - [Event Listener on Collect Elements](#event-listener-on-collect-elements) + - [UI Error for Collect Elements](#ui-error-for-collect-elements) + - [Set and Clear value for Collect Elements (DEV ENV ONLY)](#set-and-clear-value-for-collect-elements-dev-env-only) +- [Securely collecting data client-side using Composable Elements](#securely-collecting-data-client-side-using-composable-elements) + - [When to use Composable vs. Basic Elements](#when-to-use-composable-vs-basic-elements) + - [Using Skyflow Composable Elements to collect data](#using-skyflow-composable-elements-to-collect-data) + - [Using Skyflow Composable Elements to update data](#using-skyflow-composable-elements-to-update-data) +- [Securely revealing data client-side](#securely-revealing-data-client-side) + - [Using Skyflow Elements to reveal data](#using-skyflow-elements-to-reveal-data) +- [Error Handling Reference](#error-handling-reference) +- [Reporting a Vulnerability](#reporting-a-vulnerability) +# Upgrading from PDB to FlowDB +Starting in **v1.26.0-beta.1**, Collect and Reveal run on Skyflow's FlowDB backend. If you're upgrading from an earlier version, a few things changed: + +- **`CollectElementInput.altText` is deprecated and now a no-op** - it's accepted but never read or stored. Use `placeholder` instead. +- **`CollectOptions.tokens` (`Bool`) was removed** - tokens are always returned now; there's no way to opt out. +- **`CollectRecord`/`RevealRecord` field names changed**: `fields` → `tokens`, and `skyflowID` (capital `ID`) → `skyflowId`. +- **New, typed `CollectCallback`/`RevealCallback`** are now the recommended way to call `collect()`/`reveal()`, replacing the old untyped `Callback` protocol - `onSuccess`/`onFailure` now hand you typed `CollectResponse`/`RevealResponse`/`SkyflowError` objects instead of raw `Any`. See the [Error Handling Reference](#error-handling-reference) for the new `SkyflowError` shape. +- **Reveal redaction moved off `RevealElementInput`** onto `SkyflowFlowVault.RevealOptions.tokenGroupRedactions` - the old per-element `redaction`/`tokenGroupName` parameters are gone. +- **New, purely additive support** for update-by-id and upsert: `skyflowId` on `CollectElementInput`, and `SkyflowFlowVault.AdditionalFields`/`SkyflowFlowVault.UpsertOption` on `CollectOptions`. Nothing to change if you don't use them. + +# Quick Start + +### Collect data +Collect a card number and get back a token: + +```swift +import SkyflowFlowVault + +// 1. Configure and initialize the client +let config = SkyflowFlowVault.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: myTokenProvider // your SkyflowFlowVault.TokenProvider implementation - see below +) +let skyflowClient = SkyflowFlowVault.initialize(config) + +// 2. Create a container and a Collect Element for the card number +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "card_number", + type: SkyflowFlowVault.ElementType.CARD_NUMBER +) +let cardNumberElement = container?.create(input: cardNumberInput) +stackView.addArrangedSubview(cardNumberElement!) // it's a UIView - mount it like any other + +// 3. Collect the value and get back a token +let callback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + print(response.records) + }, + onFailure: { (error: SkyflowFlowVault.SkyflowError) in + print(error.message) + } +) +container?.collect(callback: callback) +``` + +### Reveal data +Reveal a token back to its real value (using the same `skyflowClient` from above): + +```swift +// 1. Create a container and a Reveal Element for the token +let revealContainer = skyflowClient.container(type: SkyflowFlowVault.ContainerType.REVEAL) +let cardNumberReveal = SkyflowFlowVault.RevealElementInput( + token: "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + label: "Card Number" +) +let revealElement = revealContainer?.create(input: cardNumberReveal) +stackView.addArrangedSubview(revealElement!) // shows the real value once revealed + +// 2. Reveal it +let revealCallback = SkyflowFlowVault.RevealCallback( + onSuccess: { (response: SkyflowFlowVault.RevealResponse) in + print(response.records) + }, + onFailure: { (error: SkyflowFlowVault.SkyflowError) in + print(error.message) + } +) +revealContainer?.reveal(callback: revealCallback) +``` + +That's the whole round trip both ways - no card data ever touches your app code on the way in, and it's only ever displayed, never returned to your code, on the way out. Everything below covers this in depth: installation, `TokenProvider`, styling, validation, updates, upsert, and more. + + +# Installation + +## Requirements +- iOS 13.0 and above +- Xcode 15 or later (Swift 5.9 toolchain) to build + +## Configuration +--- +### SPM (Swift Package Manager) +- Go to File -> Swift Packages -> New Package Dependency (in Xcode IDE) +- Enter https://github.com/skyflowapi/skyflow-iOS.git and press ok. +- In the "Choose Package Products" dialog, check the **SkyflowFlowVault** product and add it to your app target. + +### Cocoapods +- To integrate skyflow-iOS into your Xcode project using CocoaPods, specify it in your Podfile: + ``` + #Mentioning the below source will pick the podspec from Skyflow repo + #Otherwise you can add cocoapod trunk as the source + #source 'https://github.com/skyflowapi/skyflow-iOS-spec.git' + + pod 'SkyflowFlowVault' + ``` + + +# Initializing skyflow-iOS +---- +Use the ```initialize()``` method to initialize a Skyflow client as shown below. +```swift +//DemoTokenProvider is an implementation of the SkyflowFlowVault.TokenProvider protocol +let demoTokenProvider = DemoTokenProvider() + +let config = SkyflowFlowVault.Configuration( + vaultID: , + vaultURL: , + tokenProvider: demoTokenProvider, + options: SkyflowFlowVault.Options( + logLevel: SkyflowFlowVault.LogLevel.INFO, // optional — DEBUG < INFO < WARN < ERROR, default ERROR + env: SkyflowFlowVault.Env // optional, if not specified default is PROD + ) +) + +let skyflowClient = SkyflowFlowVault.initialize(config) +``` + +For the tokenProvider parameter, pass in an implementation of the `SkyflowFlowVault.TokenProvider` protocol that declares a getBearerToken method which retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. + +For example, if the response of the consumer tokenAPI is in the below format + +``` +{ + "accessToken": string, + "tokenType": string +} +``` + +then, your SkyflowFlowVault.TokenProvider Implementation should be as below + +```swift +public class DemoTokenProvider: SkyflowFlowVault.TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: ) { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) {data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + apiCallback.onFailure(error) + } + } + } + task.resume() + } + } +} +``` + +NOTE: You should pass access token as `String` value in the success callback of getBearerToken. + +For `logLevel` parameter, there are 4 accepted values in SkyflowFlowVault.LogLevel + +- `DEBUG` + + When `SkyflowFlowVault.LogLevel.DEBUG` is passed, all level of logs will be printed(DEBUG, INFO, WARN, ERROR). + +- `INFO` + + When `SkyflowFlowVault.LogLevel.INFO` is passed, INFO logs for every event that has occurred during the SDK flow execution will be printed along with WARN and ERROR logs. + +- `WARN` + + When `SkyflowFlowVault.LogLevel.WARN` is passed, WARN and ERROR logs will be printed. + +- `ERROR` + + When `SkyflowFlowVault.LogLevel.ERROR` is passed, only ERROR logs will be printed. + +`Note`: + - The ranking of logging levels is as follows : DEBUG < INFO < WARN < ERROR + - since `logLevel` is optional, by default the logLevel will be `ERROR`. + + +For `env` parameter, there are 2 accepted values in SkyflowFlowVault.Env + +- `PROD` +- `DEV` + + In [Event Listeners](#event-listener-on-collect-elements), actual value of element can only be accessed inside the handler when the `env` is set to `DEV`. + +`Note`: + - since `env` is optional, by default the env will be `PROD`. + +> **Security warning: never ship a build with `env: .DEV` to production.** `DEV` doesn't just relax a validation check - it unlocks two things that must never reach a real user: +> - The **raw, unmasked field value** becomes readable inside the `CHANGE`/`FOCUS`/`BLUR` [event listener](#event-listener-on-collect-elements) state. In `PROD`, that same state only ever contains an empty string (or, for `CARD_NUMBER`, a partially masked value) - never the full value. +> - [`setValue`/`clearValue`](#set-and-clear-value-for-collect-elements-dev-env-only) become callable and actually take effect. +> +> Both fail **silently** - no crash, no thrown error, either way. If `DEV` ships by accident, the raw PAN/CVV/etc. is simply there, with nothing to flag it. Always confirm `env` is `PROD` (or omitted) before release. + + +--- +# Securely collecting data client-side +- [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) +- [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) +- [**Using validations on Collect Elements**](#validations) +- [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) +- [**UI Error for Collect Elements**](#ui-error-for-collect-elements) +- [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) + +## Using Skyflow Elements to collect data + +**Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements in your application. + +### Step 1: Create a container + +First create a **container** for the form elements using the ```skyflowClient.container(type: SkyflowFlowVault.ContainerType)``` method as show below + +```swift +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element +To create a collect Element, we must first construct a SkyflowFlowVault.CollectElementInput object defined as shown below: + +```swift +let collectElementInput = SkyflowFlowVault.CollectElementInput( + tableName: String, // optional, the table this data belongs to + column: String, // optional, the column into which this data should be inserted + inputStyles: SkyflowFlowVault.Styles, // optional styles that should be applied to the form element + labelStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the errorText of the collect element + iconStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the card icon of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + type: SkyflowFlowVault.ElementType, // SkyflowFlowVault.ElementType enum + validations: ValidationSet, // optional set of validations for the input element + skyflowId: String, // optional, the skyflowId of the record to update +) +``` +The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. +**Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) +- `altText` (marked `(DEPRECATED)` in the signature) is dead: `CollectElementInput` accepts it but never reads or stores it, so passing a value has zero effect. `placeholder` is the real, supported way to set initial/placeholder text on a collect element. This should not be confused with `SkyflowFlowVault.RevealElementInput.altText` (used when [revealing elements](#step-2-create-a-reveal-element)), which shares the same name but is a distinct, fully supported property with no associated deprecation. + +The `inputStyles` parameter accepts a SkyflowFlowVault.Styles object which consists of multiple `SkyflowFlowVault.Styles` objects which should be applied to the form element in the following states: + +- `base`: all other variants inherit from these styles +- `complete`: applied when the Element has valid input +- `empty`: applied when the Element has no input +- `focus`: applied when the Element has focus +- `invalid`: applied when the Element has invalid input + +Each Style object accepts the following properties, please note that each property is optional: + +```swift +let style = SkyflowFlowVault.Style( + borderColor: UIColor, // optional + cornerRadius: CGFloat, // optional + padding: UIEdgeInsets, // optional + borderWidth: CGFloat, // optional + font: UIFont, // optional + textAlignment: NSTextAlignment, // optional + textColor: UIColor, // optional + boxShadow: CALayer, // optional + backgroundColor: UIColor, // optional + minWidth: CGFloat, // optional + maxWidth: CGFloat, // optional + minHeight: CGFloat, // optional + maxHeight: CGFloat, // optional + cursorColor: UIColor, // optional + width: CGFloat, // optional + height: CGFloat, // optional + placeholderColor: UIColor, // optional + cardIconAlignment: CardIconAlignment // optional, SkyflowFlowVault.CardIconAlignment enum, default is .left +) +``` + +An example SkyflowFlowVault.Styles object +```swift +let styles = SkyflowFlowVault.Styles( + base: style, // optional + complete: style, // optional + empty: style, // optional + focus: style, // optional + invalid: style, // optional + requiredAstrisk: style // optional +) +``` + +The `labelStyles` and `errorTextStyles` fields accept the above mentioned `SkyflowFlowVault.Styles` object which are applied to the `label` and `errorText` text views respectively. + +The states that are available for `labelStyles` are `base` and `focus`. + +`requiredAstrisk`: Styles applied for the Asterisk symbol in the label. Defaults to red. + +The state that is available for `errorTextStyles` is only the `base` state, it shows up when there is some error in the collect element. + +The parameters in `SkyflowFlowVault.Style` object that are respected for `label` and `errorText` text views are +- padding +- font +- textColor +- textAlignment + +Other parameters in the `SkyflowFlowVault.Style` object are ignored for `label` and `errorText` text views. + +Finally, the `type` parameter takes a SkyflowFlowVault.ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 8 types: +- `INPUT_FIELD` +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `CVV` +- `PIN` +- `EXPIRATION_YEAR` +- `EXPIRATION_MONTH` + + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. See the section on [validations](#validations) for more information on validations. + +Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. + +```swift +SkyflowFlowVault.CollectElementOptions( + required: Boolean, // Indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: Boolean, // Indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: String, // Format for the element + translation: [Character: String] // Indicates the allowed data type value for format. + enableCopy: Boolean, // Indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' + cardMetaData: [String: [SkyflowFlowVault.CardType]] // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). +) +``` + +- `required`: Indicates whether the field is marked as required or not. Default is `false`. +- `enableCardIcon`: Indicates whether the icon is visible for the CARD_NUMBER element. Default is `true`. +- `format`: A string value that indicates the format pattern applicable to the element type. Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. + For INPUT_FIELD elements, + - the length of `format` determines the expected length of the user input. + - if `translation` isn't specified, the `format` value is considered a string literal. +- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. +- `enableCopy`: Indicates whether to enable the copy icon in collect elements to copy text to clipboard. +- `cardMetaData`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow-supported card types and determines which brands display in the card number element's card brand choice dropdown. `SkyflowFlowVault.CardType` is an enum with all Skyflow-supported card schemes. + +```swift +import SkyflowFlowVault + +const cardMetaData = [ "scheme": [SkyflowFlowVault.CardType]] // Optional, array of skyflow supported card types. +``` + +#### Supported card types by SkyflowFlowVault.CardType : +- `VISA` +- `MASTERCARD` +- `AMEX` +- `DINERS_CLUB` +- `DISCOVER` +- `JCB` +- `MAESTRO` +- `UNIONPAY` +- `HIPERCARD` +- `CARTES_BANCAIRES` + +Update cardMetaData: +```swift +cardNumberElement.update(updateOptions: CollectElementOptions(cardMetaData: ["scheme": [SkyflowFlowVault.CardType.CARTES_BANCAIRES, SkyflowFlowVault.CardType.MASTERCARD]])) +``` + +Accepted values by element type: + +| Element type | `format`and `translation` values | Examples | +| -------------| ------------------------------- | -------- | +| EXPIRATION_DATE |
  • `format`
    • `mm/yy`(default)
    • `mm/yyyy`
    • `yy/mm`
    • `yyyy/mm`
    |
    • 12/27
    • 12/2027
    • 27/12
    • 2027/12
| +| EXPIRATION_YEAR |
  • `format`
    • `yyyy` (default)
    • `yy`
|
  • 27
  • 2027
| +| CARD_NUMBER |
  • `format`
    • `XXXX XXXX XXXX XXXX` (default)
    • `XXXX-XXXX-XXXX-XXXX`
|
  • 1234 5678 9012 3456
  • 1234-5678-9012-3456
| +| INPUT_FIELD |
  • `format`: A string that matches the desired output, with placeholder characters of your choice.
  • `translation`: A dictionary of key/value pairs. Defaults to `[ "X": "[0-9]"]`
| With a `format` of `+91 XXXX-XX-XXXX` and a `translation` of `[ "X": "[0-9]"]`, user input of "1234121234" displays as "+91 1234-12-1234". | + +Collect Element Options examples for INPUT_FIELD +Example 1 +```swift +SkyflowFlowVault.CollectElementOptions( + required: true, + enableCardIcon: true, + format: "+91 XXXX-XX-XXXX", + translation: [ "X": "[0-9]"] +) +``` +User input: "1234121234" + +Value displayed in INPUT_FIELD: "+91 1234-12-1234" + +Example 2 +```swift +SkyflowFlowVault.CollectElementOptions( + required: true, + enableCardIcon: true, + format: "AY XX-XXX-XXXX", + translation: [ "X": "[0-9]", "Y": "[A-Z]"] +) +``` +User input: "B1234121234" + +Value displayed in INPUT_FIELD: "AB 12-341-2123" + +Once the `SkyflowFlowVault.CollectElementInput` and `SkyflowFlowVault.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below. The `input` param takes a `SkyflowFlowVault.CollectElementInput` object as defined above and the `options` parameter takes an `SkyflowFlowVault.CollectElementOptions` object as described below: + +```swift +let collectElementInput = SkyflowFlowVault.CollectElementInput( + tableName: String, // the table this data belongs to + column: String, // the column into which this data should be inserted + inputStyles: SkyflowFlowVault.Styles, // optional styles that should be applied to the form element + labelStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the errorText of the collect element + iconStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the card icon of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + type: SkyflowFlowVault.ElementType, // SkyflowFlowVault.ElementType enum + validations: ValidationSet, // optional set of validations for the input element + skyflowId: String, // optional, the skyflowId of the record to update +) + +let collectElementOptions = SkyflowFlowVault.CollectElementOptions( + required: false, // indicates whether the field is marked as required. Defaults to 'false', + enableCardIcon: true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format: "mm/yy" // format for the element, + enableCopy: true, // indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false' +) + +let element = container?.create(input: collectElementInput, options: collectElementOptions) +``` + + + +### Step 3: Mount Elements to the Screen + +To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add it as a subview programmatically. + +```swift +let stackView = UIStackView() +stackView.addArrangedSubview(element) +``` + +The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state + +``` swift +func clearFieldsOnSubmit(_ elements: [TextField]) { + // resets all elements in the array + for element in elements { + element.unmount() + } +} +``` + +#### Step 4 : Collect data from Elements +When you submit the form, call the `collect(callback: SkyflowFlowVault.CollectCallback, options: SkyflowFlowVault.CollectOptions? = SkyflowFlowVault.CollectOptions())` method on the container object. The options parameter takes a `SkyflowFlowVault.CollectOptions` object as shown below: +```swift +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"]) +]) +// Upsert +let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords) + +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) +container?.collect(callback: insertCallback, options: options) + +``` + +### Collect data with Skyflow Elements + +#### Collect call example: +```swift + +//Initialize skyflow configuration. +let config = SkyflowFlowVault.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = SkyflowFlowVault.initialize(config) + +//Create a CollectContainer. +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) + +//Create SkyflowFlowVault.Styles with individual SkyflowFlowVault.Style variants. +let baseStyle = SkyflowFlowVault.Style(borderColor: UIColor.blue) +let baseTextStyle = SkyflowFlowVault.Style(textColor: UIColor.black) +let completeStyle = SkyflowFlowVault.Style(borderColor: UIColor.green) +let focusTextStyle = SkyflowFlowVault.Style(textColor: UIColor.red) +let inputStyles = SkyflowFlowVault.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = SkyflowFlowVault.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = SkyflowFlowVault.Styles(base: baseTextStyle) + +let iconStyles = SkyflowFlowVault.Styles(base: Style(cardIconAlignment: .left)) + +// Create a CollectElementInput. +let input = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + iconStyles: iconStyles, + label: "card number", + placeholder: "card number", + type: SkyflowFlowVault.ElementType.CARD_NUMBER +) + +// Create an option to require the element. +let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true, enableCopy: true) + +// Create a Collect Element from the Collect Container. +let skyflowElement = container?.create(input: input, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View + +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"]) +]) + + //Upsert options + let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + + +// Initialize a SkyflowFlowVault.CollectCallback - required by CollectContainer's collect(callback:options:). +// It parses onSuccess into a typed SkyflowFlowVault.CollectResponse, and onFailure into a typed +// SkyflowFlowVault.SkyflowError whenever the vault returns that structured shape (see below); +// otherwise onFailure receives the raw error (e.g. a validation SkyflowError) unchanged. +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` + +#### Skyflow returns tokens for the record you just inserted: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "skyflowId": "77dc3caf-c452-49e1-8625-07219d7567bf", + "tokens": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + } + ] +} +``` +**Note:** See the [Error Handling Reference](#error-handling-reference) for how successful and failed records are distinguished in this array, and how whole-request failures are shaped. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowId": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` + +#### If the entire request fails (for example, the vault itself can't be found): +See the [Error Handling Reference](#error-handling-reference) for the whole-request `SkyflowFlowVault.SkyflowError` shape and how to read its properties. + +## Using Skyflow Elements to update data +Updating a record uses the exact same container setup, `CollectElementInput`, `Styles`/`Style`, `CollectElementOptions`, and mounting as [collecting data](#using-skyflow-elements-to-collect-data) - see that section for the full walkthrough of all of those. The only difference for an update: pass `skyflowId` when constructing `CollectElementInput` to target an existing record instead of creating a new one. + +```swift +let collectElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" // targets an existing record for update instead of creating a new one +) +let element = container?.create(input: collectElementInput) +``` + +### Update data from Elements +When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: +- `additionalFields`: A `SkyflowFlowVault.AdditionalFields` object - non-PCI records to update or insert into the vault alongside whatever's collected from the mounted elements. +- `upsert`: An array of `SkyflowFlowVault.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `tableName`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). + +```swift +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"], skyflowId: "value") +]) +// Upsert +let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords) + +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) +container?.collect(callback: insertCallback, options: options) +``` + +**Note:** `skyflowId` is required if you want to update the data. If `skyflowId` isn't specified, the `collect(options?)` method creates a new record in the vault. + +#### Full-row overwrite with `.REPLACE` +`UpdateType.UPDATE` (used above) only merges the fields present in this request into the matched record - every other column on that record is left untouched. `UpdateType.REPLACE` instead overwrites the entire matched record: any column not included in this request's fields/`additionalFields` is cleared, not just left alone. Use `.REPLACE` when a stale value from a previous update can't be allowed to survive - for example, resetting a record to exactly what this request contains rather than layering it on top of whatever's already there. + +```swift +// Mounted element - its collected value becomes part of the replaced row, same as any +// other collect element. +let cardNumberInput = SkyflowFlowVault.CollectElementInput(tableName: "cards", column: "cardNumber", type: SkyflowFlowVault.ElementType.CARD_NUMBER) + +let cardNumberElement = container?.create(input: cardNumberInput) + +// REPLACE example: the matched "cards" record ends up with exactly cardNumber (from the element above) + cvv (from additionalFields) - any other column that record had before (e.g. a stale "expiry_date") is cleared. +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "cards", data: ["cvv": "123"]) +]) +let replaceUpsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .REPLACE)] + +let replaceOptions = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: replaceUpsertOptions) + +container?.collect(callback: insertCallback, options: replaceOptions) +``` + +### End to end example of updating data with Skyflow Elements +```swift + +//Initialize skyflow configuration. +let config = SkyflowFlowVault.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = SkyflowFlowVault.initialize(config) + +//Create a CollectContainer. +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) + +//Create SkyflowFlowVault.Styles with individual SkyflowFlowVault.Style variants. +let baseStyle = SkyflowFlowVault.Style(borderColor: UIColor.blue) +let baseTextStyle = SkyflowFlowVault.Style(textColor: UIColor.black) +let completeStyle = SkyflowFlowVault.Style(borderColor: UIColor.green) +let focusTextStyle = SkyflowFlowVault.Style(textColor: UIColor.red) +let inputStyles = SkyflowFlowVault.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = SkyflowFlowVault.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = SkyflowFlowVault.Styles(base: baseTextStyle) + +let iconStyles = SkyflowFlowVault.Styles(base: Style(cardIconAlignment: .left)) + +// Create a CollectElementInput. +let input = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + iconStyles: iconStyles, + label: "card number", + placeholder: "card number", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" +) + +// Create an option to require the element. +let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true, enableCopy: true) + +// Create a Collect Element from the Collect Container. +let skyflowElement = container?.create(input: input, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View + +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"]), + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "cards", data: ["first_name": "Joe"], skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882") +]) + + //Upsert options + let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + + +// Initialize a SkyflowFlowVault.CollectCallback - required by CollectContainer's collect(callback:options:). +// See "If the entire request fails" above for how onFailure surfaces a typed SkyflowFlowVault.SkyflowError. +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` + + +#### Skyflow returns tokens for the record you just updated: +```json +{ + "records": [ + { + "tableName": "persons", + "skyflowId": "77dc3caf-c452-49e1-8625-07219d7567bf", + "tokens": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + } + ] +} +``` +**Note:** See the [Error Handling Reference](#error-handling-reference) for how successful and failed records are distinguished in this array, and how whole-request failures are shaped. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs.", + "skyflowId": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` + +#### If the entire request fails +See the [Error Handling Reference](#error-handling-reference) for the whole-request `SkyflowFlowVault.SkyflowError` shape and how to read its properties. + +## Validations + +Skyflow-iOS provides two types of validations on Collect Elements + +#### 1. Default Validations: +Every Collect Element except of type `INPUT_FIELD` has a set of default validations listed below: +- `CARD_NUMBER`: Card number validation with checkSum algorithm(Luhn algorithm), available card lengths for defined card types +- `CARD_HOLDER_NAME`: Name should be 2 or more symbols, valid characters should match pattern - `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$` +- `CVV`: Card CVV can have 3-4 digits +- `EXPIRATION_DATE`: Any date starting from current month. By default valid expiration date should be in short year format - `MM/YY` +- `PIN`: Can have 4-12 digits + +#### 2. Custom Validations: +Custom validations can be added to any element which will be checked after the default validations have passed. The following Custom validation rules are currently supported: +- `RegexMatchRule`: You can use this rule to specify any Regular Expression to be matched with the text field value +- `LengthMatchRule`: You can use this rule to set the minimum and maximum permissible length of the textfield value +- `ElementValueMatchRule`: You can use this rule to match the value of one element with another + +The Sample code below illustrates the usage of custom validations: + +```swift +/* + Reset Password - A simple example that illustrates custom validations. + The below code shows two input fields with custom validations, + one to enter a password and the second to confirm the same password. + */ + +var myRuleset = ValidationSet() +let strongPasswordRule = RegexMatchRule( + regex: "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]*$", + error: "At least one letter and one number" +) // This rule enforces a strong password + +let lengthRule = LengthMatchRule( + minLength: 8, + maxLength: 16, + error: "Must be between 8 and 16 digits" +) // this rule allows input length between 8 and 16 characters + +// for the Password element +myRuleset.add(rule: strongPasswordRule) +myRuleset.add(rule: lengthRule) + +let collectElementOptions = CollectElementOptions(required: true) + +let passwordInput = CollectElementInput( + inputStyles: styles, + label: "password", + placeholder: "********", + type: .INPUT_FIELD, + validations: myRuleset +) +let password = container?.create(input: passwordInput, options: collectElementOptions) + +// For confirm password element - shows error when the passwords don't match +let elementValueMatchRule = ElementValueMatchRule(element: password!, error: "passwords don't match") +let confirmPasswordInput = CollectElementInput( + inputStyles: styles, + label: "Confirm password", + placeholder: "********", + type: .INPUT_FIELD, + validations: ValidationSet(rules: [strongPasswordRule, lengthRule, elementValueMatchRule]) +) +let confirmPassword = container?.create(input: confirmPasswordInput, options: collectElementOptions) + +// mount elements on screen - errors will be shown if any of the validaitons fail +stackView.addArrangedSubview(password!) +stackView.addArrangedSubview(confirmPassword!) +``` + +## Event Listener on Collect Elements +Helps to communicate with skyflow elements by listening to an event + +```swift +element!.on(eventName: SkyflowFlowVault.EventName) { _ in + // handle function +} +``` + +4 of `SkyflowFlowVault.EventName`'s events apply to an individual Element's `.on(eventName:)` listener: +- `CHANGE` + Change event is triggered when the Element's value changes. +- `READY` + Ready event is triggered when the Element is fully rendered +- `FOCUS` + Focus event is triggered when the Element gains focus +- `BLUR` + Blur event is triggered when the Element loses focus. + +`SkyflowFlowVault.EventName` has a 5th case, `SUBMIT`, but it's a no-op on an individual Element's `.on(eventName:)` - it only fires on a Composable **container's** `.on(eventName:)`, see [Set an event listener on a composable container](#set-an-event-listener-on-a-composable-container). + +The handler ```(state: [String: Any]) -> Void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. + +```swift +let state = [ + "elementType": SkyflowFlowVault.ElementType, + "isEmpty": Bool , + "isFocused": Bool, + "isValid": Bool, + "value": String, + "selectedCardScheme": String // only for CARD_NUMBER element type +] +``` +`Note:` +- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. +- `selectedCardScheme` is only populated for the `CARD_NUMBER` element state when a user chooses a card brand. By default, `selectedCardScheme` is an empty string. + +##### Sample code snippet for using listeners +```swift +// create skyflow client with loglevel:"DEBUG" +let config = SkyflowFlowVault.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: SkyflowFlowVault.Options(logLevel: SkyflowFlowVault.LogLevel.DEBUG) +) + +let skyflowClient = SkyflowFlowVault.initialize(config) + +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) + +// Create a CollectElementInput +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + ) +let cardHolderNameInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardHolderName", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + ) + +let cardNumber = container?.create(input: cardNumberInput) +let cardHolderName = container?.create(input: cardHolderNameInput) + + +// subscribing to CHANGE event, which gets triggered when element changes +cardNumber.on(eventName: SkyflowFlowVault.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} + +cardHolderName.on(eventName: SkyflowFlowVault.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} + +``` +##### Sample Element state object when `env` is `DEV` +```swift +[ + "elementType": SkyflowFlowVault.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "4111111111111111" +] +[ + "elementType": SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "John" +] + +``` +##### Sample Element state object when `env` is `PROD` +```swift +[ + "elementType": SkyflowFlowVault.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "41111111XXXXXXXX" +] +[ + "elementType": SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "" +] +``` +## UI Error for Collect Elements + +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: String)` method is used to set the error text for the element, when this method is trigerred, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is trigerred on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +##### Sample code snippet for setError and resetError + +```swift +// Create skyflow client with loglevel:"DEBUG" +let config = SkyflowFlowVault.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: SkyflowFlowVault.Options(logLevel: SkyflowFlowVault.LogLevel.DEBUG) +) + +let skyflowClient = SkyflowFlowVault.initialize(config) + +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) + +// Create a CollectElementInput +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER +) + +let cardNumber = container.create(input: cardNumberInput) + +// Set custom error +cardNumber.setError("custom error") + +// Reset custom error +cardNumber.resetError() +``` + +## Set and Clear value for Collect Elements (DEV ENV ONLY) + +`setValue(value: String)` method is used to set the value of the element. This method will override any previous value present in the element. + +`clearValue()` method is used to reset the value of the element. + +`Note:` This methods are only available in DEV env for testing/developmental purposes and MUST NOT be used in PROD env. + +##### Sample code snippet for setValue and clearValue + +```swift +// Create skyflow client with env DEV +let config = SkyflowFlowVault.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: SkyflowFlowVault.Options(env: SkyflowFlowVault.Env.DEV) +) + +let skyflowClient = SkyflowFlowVault.initialize(config) + +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COLLECT) + +// Create a CollectElementInput +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER +) + +let cardNumber = container.create(input: cardNumberInput) + +// Set a value programatically +cardNumber.setValue("4111111111111111") + +// Clear the value +cardNumber.clearValue() +``` + +# Securely collecting data client-side using Composable Elements +- [Using Skyflow Composable Elements to collect data](#using-skyflow-composable-elements-to-collect-data) +- [Using Skyflow Composable Elements to update data](#using-skyflow-composable-elements-to-update-data) + +## When to use Composable vs. Basic Elements +Use **Composable Elements** when you want multiple fields laid out together in a shared row - for example, expiration month and year, or expiration date and CVV, side by side in the same visual container. Use **Basic Elements** (the [section above](#securely-collecting-data-client-side)) when each field is its own standalone view that you place and style individually. + +Beyond layout, the two are functionally the same: `CollectElementInput`, `CollectElementOptions`, `collect()`, and `SkyflowFlowVault.CollectOptions` (`additionalFields`, `upsert`) all work identically for both. Composable Elements just add a `ContainerOptions.layout` array to group elements into rows, plus a `getComposableView()` call to mount the whole group at once instead of mounting each element separately. + +## Using Skyflow Composable Elements to collect data +Composable Elements combine multiple Skyflow Elements in a single row. The following steps create a composable element and securely collect data through it. + +### Step 1: Create a composable container + +First create a **container** for the form elements using the ```skyflowClient.container(type: SkyflowFlowVault.ContainerType)``` method as show below + +```swift +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: ContainerOptions) +``` + +The container requires an options object that contains the following keys: + +- `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. + + For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. + + `Note`: The sum of values in the layout array should be equal to the number of elements created + +- `styles`: styles to apply to each composable row. + +- `errorTextStyles`: styles to apply if an error is encountered. + +```swift +var containerOptions = ContainerOptions( + layout: [1,1,2], // required + styles: SkyflowFlowVault.Styles, // optional + errorTextStyles: SkyflowFlowVault.Styles //optional + ) +``` +### Step 2: Create Composable Elements +Composable Elements use the same `SkyflowFlowVault.CollectElementInput` and `SkyflowFlowVault.CollectElementOptions` schema as [basic Collect Elements](#step-2-create-a-collect-element) - see that section for the full parameter list, the `Styles`/`Style` walkthrough, and `CollectElementOptions` (including `cardMetaData`/`SkyflowFlowVault.CardType` and the `format`/`translation` tables). + +The iOS SDK supports the following composable elements: + +- `INPUT_FIELD` +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `CVV` +- `PIN` +- `EXPIRATION_YEAR` +- `EXPIRATION_MONTH` + +**Note**: Only when the entered value in the below composable elements is valid, the focus shifts automatically. The element types are: + +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` + +Once the `SkyflowFlowVault.CollectElementInput` and `SkyflowFlowVault.CollectElementOptions` objects are defined, add to the container using the ```create(input: CollectElementInput, options: CollectElementOptions)``` method as shown below: + +```swift +let composableElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER +) + +let collectElementOptions = SkyflowFlowVault.CollectElementOptions(required: true) + +let element = container?.create(input: composableElementInput, options: collectElementOptions) +``` +### Step 3: Mount Elements to the Screen + +To specify where the Elements will be rendered on the screen, create a parent UIView (like UIStackView, etc) and you can add composable elements view using `container?.getComposableView()`, it as a subview programmatically. + +```swift +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +``` + +The Skyflow Element is an implementation of the UIView so it can be used/mounted similarly. Alternatively, you can use the `unmount` method to reset any collect element to it's initial state + +``` swift +func clearFieldsOnSubmit(_ elements: [TextField]) { + // resets all elements in the array + for element in elements { + element.unmount() + } +} +``` +### Step 4: Collect data from elements +When you submit the form, call the `collect(callback: SkyflowFlowVault.CollectCallback, options: SkyflowFlowVault.CollectOptions? = SkyflowFlowVault.CollectOptions())` method on the container object. +The options parameter takes a `SkyflowFlowVault.CollectOptions` object as shown below: + +- `additionalFields`: A `SkyflowFlowVault.AdditionalFields` object - non-PCI records to insert into the vault alongside whatever's collected from the mounted elements. +- `upsert`: An array of `SkyflowFlowVault.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `tableName`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it - see [Full-row overwrite with `.REPLACE`](#full-row-overwrite-with-replace) for a worked example). + +```swift +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"]) +]) +// Upsert +let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) +container?.collect(callback: insertCallback, options: options) +``` +End to end example of collecting data with Composable Elements + +```swift + +//Initialize skyflow configuration. +let config = SkyflowFlowVault.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = SkyflowFlowVault.initialize(config) + +let containerOptions = ContainerOptions(layout: [1,2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: containerOptions) + +//Create SkyflowFlowVault.Styles with individual SkyflowFlowVault.Style variants. +let baseStyle = SkyflowFlowVault.Style(borderColor: UIColor.blue) +let baseTextStyle = SkyflowFlowVault.Style(textColor: UIColor.black) +let completeStyle = SkyflowFlowVault.Style(borderColor: UIColor.green) +let focusTextStyle = SkyflowFlowVault.Style(textColor: UIColor.red) +let inputStyles = SkyflowFlowVault.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = SkyflowFlowVault.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = SkyflowFlowVault.Styles(base: baseTextStyle) + +// Create Composable Elements. + +let cardHolderNameElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "first_name", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "first name", + placeholder: "first name", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME +) +// Create an option to require the element. +let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) + +// Create a Composable Element from the Composable Container. +let cardHolderNameElement = container?.create(input: cardHolderNameElementInput, options: requiredOption) + + +let cardNumberElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "card number", + placeholder: "card number", + type: SkyflowFlowVault.ElementType.CARD_NUMBER +) + +let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) + +let cvvElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cvv", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cvv", + placeholder: "cvv", + type: SkyflowFlowVault.ElementType.CVV +) + +let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} + +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"]) +]) + + //Upsert options +let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + + +// Initialize a SkyflowFlowVault.CollectCallback - required by CollectContainer's collect(callback:options:). +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` +### Sample Response: + +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "skyflowId": "77dc3caf-c452-49e1-8625-07219d7567bf", + "tokens": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + } + ] +} +``` +**Note:** See the [Error Handling Reference](#error-handling-reference) for how successful and failed records are distinguished in this array, and how whole-request failures are shaped. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowId": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` +[For information on validations, see validations.](#validations) + + + +## Set an event listener on Composable Elements: +You can communicate with Skyflow Elements by listening to element events: + +```swift +element!.on(eventName: SkyflowFlowVault.EventName) { _ in + // handle function +} +``` + +4 of `SkyflowFlowVault.EventName`'s events apply to an individual Element's `.on(eventName:)` listener: + +- CHANGE: Triggered when the Element's value changes. +- READY: Triggered when the Element is fully rendered. +- FOCUS: Triggered when the Element gains focus. +- BLUR: Triggered when the Element loses focus. + +`SkyflowFlowVault.EventName` has a 5th case, `SUBMIT`, but it's a no-op on an individual Element's `.on(eventName:)` - it only fires on a Composable **container's** `.on(eventName:)`, see [Set an event listener on a composable container](#set-an-event-listener-on-a-composable-container) below. + +The handler ```(state: [String: Any]) -> Void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. + +```swift +let state = [ + "elementType": SkyflowFlowVault.ElementType, + "isEmpty": Bool , + "isFocused": Bool, + "isValid": Bool, + "value": String +] +``` + +`Note`: +values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. + +#### Example Usage of Event Listener on Composable Elements + +```swift +// create skyflow client with loglevel:"DEBUG" +let config = SkyflowFlowVault.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: SkyflowFlowVault.Options(logLevel: SkyflowFlowVault.LogLevel.DEBUG) +) + +let skyflowClient = SkyflowFlowVault.initialize(config) + +let containerOptions = ContainerOptions(layout: [2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: containerOptions) + +// Create a CollectElementInput +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + ) +let cardHolderNameInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardHolderName", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + ) + +let cardNumber = container?.create(input: cardNumberInput) +let cardHolderName = container?.create(input: cardHolderNameInput) + +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +// subscribing to CHANGE event, which gets triggered when element changes +cardNumber.on(eventName: SkyflowFlowVault.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} + +cardHolderName.on(eventName: SkyflowFlowVault.EventName.CHANGE) { state in + // Your implementation when Change event occurs + print(state) +} +``` + +##### Sample Element state object when `env` is `DEV` +```swift +[ + "elementType": SkyflowFlowVault.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "4111111111111111" +] +[ + "elementType": SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "John" +] + +``` +##### Sample Element state object when `env` is `PROD` +```swift +[ + "elementType": SkyflowFlowVault.ElementType.CARD_NUMBER, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "41111111XXXXXXXX" +] +[ + "elementType": SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + "isEmpty": false, + "isFocused": true, + "isValid": true, + "value": "" +] +``` +### Update composable elements +You can update composable element properties with the `update` interface. + +The `update` interface takes the below object: +```swift +let updateElement = SkyflowFlowVault.CollectElementInput( + tableName: String, // optional the table this data belongs to + column: String, // optional the column into which this data should be inserted + inputStyles: SkyflowFlowVault.Styles, // optional styles that should be applied to the form element + labelStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the label of the collect element + errorTextStyles: SkyflowFlowVault.Styles, // optional styles that will be applied to the errorText of the collect element + label: String, // optional label for the form element + placeholder: String, // optional placeholder for the form element + altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element + validations: ValidationSet, // optional set of validations for the input element +) +``` +Only include the properties that you want to update for the specified composable element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +**Note:** `altText` above is a no-op, same as on `CollectElementInput` - see [Step 2: Create a collect Element](#step-2-create-a-collect-element) for why, and what to use instead. + +`Note`: You can't update the type property of an element. + +### End to end example +```swift +// create skyflow client with loglevel:"DEBUG" +let config = SkyflowFlowVault.Configuration( + vaultID: VAULT_ID, + vaultURL: VAULT_URL, + tokenProvider: demoTokenProvider, + options: SkyflowFlowVault.Options(logLevel: SkyflowFlowVault.LogLevel.DEBUG) +) + +let skyflowClient = SkyflowFlowVault.initialize(config) + +let containerOptions = ContainerOptions(layout: [2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: containerOptions) + +// Create a CollectElementInput +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + ) +let cardHolderNameInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardHolderName", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + ) + +let cardNumber = container?.create(input: cardNumberInput) +let cardHolderName = container?.create(input: cardHolderNameInput) + +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +// Update table, column, inputStyles properties on cardNumber. +cardNumber.update(update: CollectElementInput( + tableName: "cards", + column: "cardHolderName", + inputStyles: SkyflowFlowVault.Styles(base: Style(borderColor: UIColor.red)) +)) +let lengthRule = LengthMatchRule( + minLength: 5, + maxLength: 16, + error: "Must be between 5 and 16 digits") + +// Update validations and placeholder property on cardHolderName. +cardHolderName.update(update: CollectElementInput( + placeholder: "cardHolderName", + validations: ValidationSet(rules: [lengthRule]) +)) +``` + +### Set an event listener on a composable container + +Currently, the SDK supports one event: + + - `SUBMIT`: Triggered when the Enter key is pressed in any container element. + +The handler function(void) -> void is a callback function you provide that's called when the `SUBMIT` event fires. + +#### Example + +```swift +let containerOptions = ContainerOptions(layout: [1], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let composableContainer = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: containerOptions) + +// Creating the element. +let cardNumberInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + ) +let cardNumber = composableContainer?.create(input: cardNumberInput) + +// get composable view and mount it +let stackView = UIStackView() +do { + let composableView = try composableContainer?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} +// Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. +composableContainer?.on(eventName: .SUBMIT){ + // Your implementation when the SUBMIT(enter) event occurs. + print("Submit Event Listener is being Triggered.") +} + +``` + +## Using Skyflow Composable Elements to update data +Updating with Composable Elements uses the exact same container setup, `CollectElementInput`, `CollectElementOptions`, and mounting as [collecting data with Composable Elements](#using-skyflow-composable-elements-to-collect-data) - see that section for the full walkthrough. The only difference for an update: pass `skyflowId` when constructing `CollectElementInput` to target an existing record instead of creating a new one. + +```swift +let composableElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" // targets an existing record for update instead of creating a new one +) +let element = container?.create(input: composableElementInput) +``` + +### Update data from Elements +When you submit the form, call the `collect(callback: SkyflowFlowVault.CollectCallback, options: SkyflowFlowVault.CollectOptions? = SkyflowFlowVault.CollectOptions())` method on the container object. +The options parameter takes a `SkyflowFlowVault.CollectOptions` object as shown below: + +- `additionalFields`: A `SkyflowFlowVault.AdditionalFields` object - non-PCI records to insert into the vault alongside whatever's collected from the mounted elements. +- `upsert`: An array of `SkyflowFlowVault.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `tableName`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it - see [Full-row overwrite with `.REPLACE`](#full-row-overwrite-with-replace) for a worked example). + +```swift +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"]) +]) +// Upsert +let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) +container?.collect(callback: insertCallback, options: options) +``` +End to end example of collecting data with Composable Elements + +```swift + +//Initialize skyflow configuration. +let config = SkyflowFlowVault.Configuration(vaultID: VAULT_ID, vaultURL: VAULT_URL, tokenProvider: demoTokenProvider) + +//Initialize skyflow client. +let skyflowClient = SkyflowFlowVault.initialize(config) + +let containerOptions = ContainerOptions(layout: [1,2], styles: Styles(base: Style(borderColor: UIColor.gray)), errorTextStyles: Styles(base: Style(textColor: UIColor.red))) +//Create a Composable Container. +let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: containerOptions) + +//Create SkyflowFlowVault.Styles with individual SkyflowFlowVault.Style variants. +let baseStyle = SkyflowFlowVault.Style(borderColor: UIColor.blue) +let baseTextStyle = SkyflowFlowVault.Style(textColor: UIColor.black) +let completeStyle = SkyflowFlowVault.Style(borderColor: UIColor.green) +let focusTextStyle = SkyflowFlowVault.Style(textColor: UIColor.red) +let inputStyles = SkyflowFlowVault.Styles(base: baseStyle, complete: completeStyle) +let labelStyles = SkyflowFlowVault.Styles(base: baseTextStyle, focus: focusTextStyle) +let errorTextStyles = SkyflowFlowVault.Styles(base: baseTextStyle) + +// Create Composable Elements. + +let cardHolderNameElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "first_name", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "first name", + placeholder: "first name", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" +) +// Create an option to require the element. +let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) + +// Create a Composable Element from the Composable Container. +let cardHolderNameElement = container?.create(input: cardHolderNameElementInput, options: requiredOption) + + +let cardNumberElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cardNumber", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "card number", + placeholder: "card number", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" +) + +let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) + +let cvvElementInput = SkyflowFlowVault.CollectElementInput( + tableName: "cards", + column: "cvv", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cvv", + placeholder: "cvv", + type: SkyflowFlowVault.ElementType.CVV, + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" +) + +let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) + +// Can interact with this object as a normal UIView Object and add to View +let stackView = UIStackView() +do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) +} catch { + print(error) +} + +// Non-PCI records +let nonPCIRecords = SkyflowFlowVault.AdditionalFields(records: [ + SkyflowFlowVault.AdditionalFieldsRecord(tableName: "persons", data: ["gender": "MALE"], skyflowId: "77dc3caf-c452-49e1-8625-07219d7567bf") +]) + + //Upsert options + let upsertOptions = [SkyflowFlowVault.UpsertOption(tableName: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] + +// Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. +let collectOptions = SkyflowFlowVault.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + +// Initialize a SkyflowFlowVault.CollectCallback - required by CollectContainer's collect(callback:options:). +let insertCallback = SkyflowFlowVault.CollectCallback( + onSuccess: { (response: SkyflowFlowVault.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) + +// Call collect method on CollectContainer. +container?.collect(callback: insertCallback, options: collectOptions) +``` +### Sample Success Response: +```json +{ + "records": [ + { + "tableName": "persons", + "skyflowId": "77dc3caf-c452-49e1-8625-07219d7567bf", + "tokens": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}], + "cvv": [{"token": "098834fe-de99-4fc8-abdf-88c18a28a2cf", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + } + ] +} +``` +### Sample Partial Error Response: +See the [Error Handling Reference](#error-handling-reference) for how successful and failed records are distinguished in this array, and how whole-request failures are shaped. +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}], + "cvv": [{"token": "098834fe-de99-4fc8-abdf-88c18a28a2cf", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Update failed. skyflowIds [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs.", + "skyflowId": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` + +If the entire request fails (for example, the vault itself can't be found), `onFailure` receives a single structured `SkyflowFlowVault.SkyflowError` instead of a `records` array - see the [Error Handling Reference](#error-handling-reference) for that shape. + +# Securely revealing data client-side +- [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) +- [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) +- [**Set token for Reveal Elements**](#set-token-for-reveal-elements) +- [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) + +## Using Skyflow Elements to reveal data +Skyflow Elements can be used to securely reveal data in an application without exposing your front end to the sensitive data. This is great for use-cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. +### Step 1: Create a container +To start, create a container using the `skyflowClient.container(SkyflowFlowVault.ContainerType.REVEAL)` method as shown below. +```swift + let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.REVEAL) +``` + +### Step 2: Create a reveal Element +To create a reveal Element, we must first construct a SkyflowFlowVault.RevealElementInput object defined as shown below: + +```swift +let revealElementInput = SkyflowFlowVault.RevealElementInput( + token: String, // optional, token of the data being revealed + inputStyles: SkyflowFlowVault.Styles(), // optional, styles to be applied to the element + labelStyles: SkyflowFlowVault.Styles(), // optional, styles to be applied to the label of the reveal element + errorTextStyles: SkyflowFlowVault.Styles(), // optional styles that will be applied to the errorText of the reveal element + label: "cardNumber", // required, label for the element, + altText: "XXXX XXXX XXXX XXXX", // optional, string that is shown before reveal, will show token if it is not provided +) +``` +`Note`: +- To apply a redaction to a token group as part of the detokenize call, use `tokenGroupRedactions` on `SkyflowFlowVault.RevealOptions` (passed to `reveal(callback:options:)`) rather than on the individual `RevealElementInput` - see [Step 4: Reveal data](#step-4-reveal-data). If not provided, the vault-configured default redaction is applied. Supported redaction values are `PLAIN_TEXT`, `MASKED`, `REDACTED` and `DEFAULT`. + + +The `inputStyles` parameter accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but the only state available for a reveal element is the base state. + +The `labelStyles` and `errorTextStyles` fields accept the above mentioned `SkyflowFlowVault.Styles` object as described in the [previous section](#step-2-create-a-collect-element), the only state available for a reveal element is the base state. + +The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but only a single variant is available i.e. base. + +An example of a inputStyles object: + +```swift +let inputStyles = SkyflowFlowVault.Styles(base: SkyflowFlowVault.Style(borderColor: UIColor.green)) +``` + +An example of a labelStyles object: + +```swift +let labelStyles = SkyflowFlowVault.Styles(base: SkyflowFlowVault.Style(font: UIFont (name: "GILLSANSCE-ROMAN", size: 12)))) +``` + +An example of a errorTextStyles object: + +```swift +let labelStyles = SkyflowFlowVault.Styles(base: SkyflowFlowVault.Style(textColor: UIColor.red)) +``` +Along with `RevealElementInput`, you can define other options in the `RevealElementOptions` object as described below: +```swift +SkyflowFlowVault.RevealElementOptions( + format: String, // Format for the element. + translation: [Character: String] // Indicates the allowed data type value for format + enableCopy: Boolean, // Indicates whether to enable the copy icon in reveal elements to copy text to clipboard. Defaults to 'false' +) +``` +- `format`: A string value that indicates how the element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified, the `format` value is considered a string literal. +- `translation`: A dictionary of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `[ "X": "[0-9]"]`. +- `enableCopy`: Indicates whether to enable the copy icon in reveal elements to copy text to clipboard. + +Reveal Element Options examples: + +Example 1: +```swift +SkyflowFlowVault.RevealElementOptions( + format: "(XXX) XXX-XXXX", + translation: [ "X": "[0-9]"] +) +``` +Value from vault: "1234121234" + +Value displayed in element: "(123) 412-1234" + +Example 2: +```swift +SkyflowFlowVault.RevealElementOptions( + format: "XXXX-XXXXXX-XXXXX", + translation: [ "X": "[0-9]"] +) +``` +Value from vault: "374200000000004" + +Value displayed in element: "3742-000000-00004" + +Once you've defined a `SkyflowFlowVault.RevealElementInput` object and `SkyflowFlowVault.RevealElementOptions`, you can use the `create()` method of the container to create the Element as shown below: + +```swift +let element = container.create(input: revealElementInput, options: SkyflowFlowVault.RevealElementOptions(format: "XXXX-XXXXXX-XXXXX", +translation: ["X": "[0-9]"] +)) +``` + +### Step 3: Mount Elements to the Screen + +Elements used for revealing data are mounted to the screen the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-screen). + +### Step 4: Reveal data +When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: +```swift +let revealCallback = SkyflowFlowVault.RevealCallback( + onSuccess: { (response: SkyflowFlowVault.RevealResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) +container.reveal(callback: revealCallback) +``` + +To apply a redaction to one or more token groups as part of the detokenize call, pass `tokenGroupRedactions` via `SkyflowFlowVault.RevealOptions`: +```swift +let revealOptions = SkyflowFlowVault.RevealOptions( + tokenGroupRedactions: [ + SkyflowFlowVault.TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + ] +) +container.reveal(callback: revealCallback, options: revealOptions) +``` +This is a request-level setting - the redaction applies to every token in the named group, not to individual reveal elements. + +### UI Error for Reveal Elements + +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: String)` method is used to set the error text for the element, when this method is trigerred, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is trigerred on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +### Set token for Reveal Elements + +The `setToken(value: String)` method can be used to set the token of the Reveal Element. If no altText is set, the set token will be displayed on the UI as well. If altText is set, then there will be no change in the UI but the token of the element will be internally updated. + +### Set and Clear altText for Reveal Elements + +The `setAltText(value: String)` method can be used to set the altText of the Reveal Element. This will cause the altText to be displayed in the UI regardless of whether the token or value is currently being displayed. + +`clearAltText()` method can be used to clear the altText, this will cause the element to display the token or actual value of the element. If the element has no token, the element will be empty. + +### Reveal data with Skyflow Elements +#### End-to-end example of revealing data with Skyflow Elements +```swift +// Initialize skyflow configuration +let config = SkyflowFlowVault.Configuration(vaultID: , vaultURL: , tokenProvider: demoTokenProvider) + +// Initialize skyflow client +let skyflowClient = SkyflowFlowVault.initialize(config) + +// Create a Reveal Container +let container = skyflowClient.container(type: SkyflowFlowVault.ContainerType.REVEAL) + +// Create SkyflowFlowVault.Styles with individual SkyflowFlowVault.Style variants +let baseStyle = SkyflowFlowVault.Style(borderColor: UIColor.blue) +let baseTextStyle = SkyflowFlowVault.Style(textColor: UIColor.BLACK) +let inputStyles = SkyflowFlowVault.Styles(base: baseStyle) +let labelStyles = SkyflowFlowVault.Styles(base: baseTextStyle) +let errorTextStyles = SkyflowFlowVault.Styles(base: baseTextStyle) + +// Create Reveal Elements +let cardNumberInput = SkyflowFlowVault.RevealElementInput( + token: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cardnumber", + altText: "XXXX XXXX XXXX XXXX" +) + +let cardNumberElement = container?.create(input: cardNumberInput) + +let cvvInput = SkyflowFlowVault.RevealElementInput( + token: "89024714-6a26-4256-b9d4-55ad69aa4047", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "cvv", + altText: "XXX" +) +let cvvElement = container?.create(input: cvvInput) + +let expiryDateInput = SkyflowFlowVault.RevealElementInput( + token: "a4b24714-6a26-4256-b9d4-55ad69aa4047", + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: "expiryDate", + altText: "MM/YYYY" +) +let expiryDateElement = container?.create(input: expiryDateInput) + +// Can interact with these objects as a normal UIView Object and add to View + +// set error to the element +cvvElement!.setError("custom error") +// reset error to the element +cvvElement!.resetError() + +// Initialize a SkyflowFlowVault.RevealCallback - required by RevealContainer's reveal(callback:options:). +let revealCallback = SkyflowFlowVault.RevealCallback( + onSuccess: { (response: SkyflowFlowVault.RevealResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record) + } + } + }, + onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) + +// Optional: apply a redaction to a token group as part of the detokenize call +let revealOptions = SkyflowFlowVault.RevealOptions( + tokenGroupRedactions: [ + SkyflowFlowVault.TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + ] +) + +// Call reveal method on RevealContainer +container?.reveal(callback: revealCallback, options: revealOptions) + +``` + +#### Sample Response: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + } + ] +} +``` + +#### Sample Partial Error Response: +Some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. See the [Error Handling Reference](#error-handling-reference) for how successful and failed records are distinguished in this array, and how whole-request failures are shaped: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] +} +``` + +## Error Handling Reference + +Collect and Reveal both share the same two failure shapes. They're described once here rather than repeated at every call site - the Collect/Reveal sections above link back to this section instead of re-explaining it. + +### Per-record failures + +`collect()`/`reveal()` never split successes and failures into separate arrays or separate callbacks - every record (or token) comes back together in the same `records` array passed to `onSuccess`, each entry carrying its own `httpCode`. A successful entry carries the flow's data field (`tokens` for Collect, `metadata` for Reveal); a failed entry carries `error` instead, and omits the data field entirely. Check for the presence of `error` to tell the two apart - don't rely on `httpCode` alone. + +Collect shape: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowId": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` + +Reveal shape: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] +} +``` + +`SkyflowFlowVault.CollectRecord`/`SkyflowFlowVault.RevealRecord` expose all of the above as typed properties (`tableName`/`skyflowId`/`tokens`/`hashedData`/`httpCode`/`error` and `token`/`tokenGroupName`/`metadata`/`httpCode`/`error` respectively) - you don't need to read the raw dictionary shown above yourself. + +### Whole-request failures (`SkyflowFlowVault.SkyflowError`) + +If the *entire* request fails - vault not found, network error, invalid bearer token - or a client-side validation failure occurs (empty `vaultID`, unmounted element, etc.) - `onFailure` is called instead of `onSuccess`, with a single `SkyflowFlowVault.SkyflowError` rather than a `records` array. + +The vault's wire format for a whole-request failure is: +```json +{ + "error": { + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] + } +} +``` + +`onFailure` never hands you this dictionary as-is - it's always normalized into a `SkyflowFlowVault.SkyflowError` (an `NSError` subclass) with these same fields exposed as flat properties, not nested under an `error` key: +```swift +onFailure: { (skyflowError: SkyflowFlowVault.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") +} +``` +`grpcCode`, `httpStatus`, and `details` are only populated for this whole-request-failure shape - they're `nil` for client-side validation failures (empty `vaultID`, unmounted element, missing required field, etc.), which only populate `httpCode` and `message`: +- `httpCode` is a client-assigned code describing the kind of problem (typically `400`, sometimes `404` for a "not found"-shaped issue like an empty records array) - it was never returned by an actual HTTP response, so don't treat it as a real server status. +- `message` is a description prefixed with the SDK name and version, e.g. `"iOS SDK v1.26.0-beta.1 Validation error.'table' key not found in collect element. Specify a valid value for 'table' key."` + +In short: if `grpcCode`/`httpStatus`/`details` are all `nil`, you're looking at a client-side validation failure (bad input, caught before any network call) - use `message` to see which check failed. If they're populated, the vault itself rejected or couldn't process the request. + +## Reporting a Vulnerability + +If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. \ No newline at end of file diff --git a/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/data.mdb b/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/data.mdb new file mode 100644 index 0000000000000000000000000000000000000000..aa89ad86a084cdab2661547bd094a70b0d905660 GIT binary patch literal 67108864 zcmeF)JC4&(6adi2XoQFuO-Ta<3&4niYLpK(yGV#<#v-4Q?SRryP_qt}fe>N^EPw^j zu!I5s62;O)I0y-ij*C3MkMFzp*eZ*5lkcB@e!9~PRqb)vya=IMu{+uBUDro@ z{HA`hzP0B~%Z(EtK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB=D8DX{(S{$G6mzm2{WkDiV`4u1^a_3sY8 z^j{9Xj#`)M5+Fc;009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAn@M^Jo}x=Q8)?n<#aWRqs4e0XGL<=|A_BT^SVBo z82LE8i1WXTv05Z~G+9SQTkPK7pF%m?&ZCKOp3LIIV)Q~8BUzj;ivV3PLZxvUuF~b3 zWE!VYkw$6ysLXc%&u%RbLUHlcIKRvalxdR2ukvNOE@E_->6OtAy2W)>_d}CH()}^1 zW1NI+J)bOR(d%R}EnT`-vWc-N=J|fsa(37D;pPry(c2`yjPf|kqbj$~uRvW}ZyU4N z@qp*%IIOa?^V9MN#alvv009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7e?FLvla000000LcHf5fU6YaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2e3m20ssI206_k)jga8LfddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIDj2G5C8xG008oTZG;2|4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`zya*gfdBvi005BxYa=8$ zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M%C|4g>%I004meUmGF8fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB)i;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?utNs| z00000K>n|dkl?_90|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KfE_vz000000P=rrgaii;95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=0qoF$000000FeJ{BP2L*;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4q%541ONa40D$~o8zI4g0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KZ~!}WAOHXW0089w+6V~_ z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fdks1V*vmF0DwXM)`Qqv2pKS7z<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ f0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz`z&?wch3e literal 0 HcmV?d00001 diff --git a/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/lock.mdb b/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/lock.mdb new file mode 100644 index 0000000000000000000000000000000000000000..36898a978b6aa290820fe0368a676bcf2403a638 GIT binary patch literal 8256 zcmeIuJq|!X5P(rc?F?Fto9JjH&cF>gf;fT1J;X`0N-p3xyDNeS(SF_W?oYr`&xvNw4=lfXuW=?vVqvr4b4pFRj+20<5 zYk&2#Kl|686rcJZRwIP~0tg_000IagfB*srAb /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A51B28A216E438D119DD19DB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CollectAndRevealSample/Pods-CollectAndRevealSample-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CollectAndRevealSample/Pods-CollectAndRevealSample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CollectAndRevealSample/Pods-CollectAndRevealSample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F1613E48272565CF0042C130 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F1613E54272565CF0042C130 /* ViewController.swift in Sources */, + F1613E6B272566800042C130 /* ResponseStructs.swift in Sources */, + F1613E682725665D0042C130 /* ExampleTokenProvider.swift in Sources */, + F1613E50272565CF0042C130 /* AppDelegate.swift in Sources */, + F1613E652725663C0042C130 /* ExampleAPICallback.swift in Sources */, + F1613E52272565CF0042C130 /* SceneDelegate.swift in Sources */, + 7C2C89DE2C8F23C30076C641 /* CollectAndRevealViewController.swift in Sources */, + 7C2C89D82C89C9520076C641 /* CardBrandChoiceSampleViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + F1613E55272565CF0042C130 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F1613E56272565CF0042C130 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + F1613E5A272565D30042C130 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F1613E5B272565D30042C130 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F1613E5E272565D30042C130 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F1613E5F272565D30042C130 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F1613E61272565D30042C130 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 267485426163B1ACCD8ED25D /* Pods-CollectAndRevealSample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = CollectAndRevealSample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.CollectAndRevealSample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F1613E62272565D30042C130 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F2D4C9A9B1D1DF56AC7781D1 /* Pods-CollectAndRevealSample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = CollectAndRevealSample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.CollectAndRevealSample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F1613E47272565CF0042C130 /* Build configuration list for PBXProject "CollectAndRevealSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F1613E5E272565D30042C130 /* Debug */, + F1613E5F272565D30042C130 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F1613E60272565D30042C130 /* Build configuration list for PBXNativeTarget "CollectAndRevealSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F1613E61272565D30042C130 /* Debug */, + F1613E62272565D30042C130 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 7C2C89DF2C8F25360076C641 /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.23.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + }; + rootObject = F1613E44272565CF0042C130 /* Project object */; +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..3c38cba2 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift new file mode 100644 index 00000000..d1190a36 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/AppDelegate.swift @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift new file mode 100644 index 00000000..aad636c3 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift @@ -0,0 +1,147 @@ +// +// CardBrandChoiceSampleViewController.swift +// CollectAndRevealSample +// +// Created by Bharti Sagar on 05/09/24. +// + +import Foundation +import UIKit +import SkyflowFlowVault + +class CardBrandChoiceSampleViewController: UIViewController { + + private var skyflowClient: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var collectCardNumberElement: TextField! + + private var stackView: UIStackView! + + override func viewDidLoad(){ + super.viewDidLoad() + view.backgroundColor = .white + self.stackView = UIStackView() + let config = SkyflowFlowVault.Configuration(vaultID: "", vaultURL: "", tokenProvider: ExampleTokenProvider(), options: SkyflowFlowVault.Options(env: SkyflowFlowVault.Env.PROD)) + skyflowClient = SkyflowFlowVault.initialize(config) + if self.skyflowClient != nil { + container = self.skyflowClient?.container(type: ContainerType.COLLECT, options: nil) + + + // styles for elements + let uiFont = UIFont.systemFont(ofSize: 20, weight: .light) + let baseStyle = SkyflowFlowVault.Style(borderColor: .gray,cornerRadius: 5, padding: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),borderWidth: 0.5, font: uiFont, textAlignment: .left, textColor: .black, backgroundColor: .white, maxWidth: 50, placeholderColor: .gray) + let focusStyle = SkyflowFlowVault.Style(borderColor: .black, cornerRadius: 5, padding: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),borderWidth: 0.5, font: uiFont, textAlignment: .left, textColor: .black)//, backgroundColor: .yellow) + let completeStyle = SkyflowFlowVault.Style(borderColor: .black,cornerRadius: 5, padding: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),borderWidth: 0.5, font: uiFont, textAlignment: .left, textColor: .black) + let invalidStyle = SkyflowFlowVault.Style(borderColor: .black,cornerRadius: 5, padding: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),borderWidth: 0.5, font: uiFont, textAlignment: .left, textColor: .black) + let emptyStyle = SkyflowFlowVault.Style(borderColor: .black,cornerRadius: 5, padding: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),borderWidth: 0.5, font: uiFont, textAlignment: .left, textColor: .black) + + let inputStyles = SkyflowFlowVault.Styles(base: baseStyle, complete: completeStyle, empty: emptyStyle, focus: focusStyle, invalid: invalidStyle) + let iconStyles = SkyflowFlowVault.Styles(base: Style(cardIconAlignment: .right)) + + // create card number element + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput(table: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: SkyflowFlowVault.ElementType.CARD_NUMBER) + let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true, enableCardIcon: true, enableCopy: false) + + collectCardNumberElement = container?.create(input: collectCardNumberInput, options: requiredOption) + + // Cardnumber element change listener + var calledUpdate = false; + self.collectCardNumberElement.on(eventName: SkyflowFlowVault.EventName.CHANGE) { state in + print("CHANGED", state) + if((state["value"] as! String).count >= 8 && !calledUpdate){ + calledUpdate = true + self.binLookup(bin: String((state["value"] as! String).prefix(8))) { result in + switch result { + case .success(let schemeList): + DispatchQueue.main.async { + // Update the card number element with the scheme list using update Method + self.collectCardNumberElement.update(updateOptions: CollectElementOptions(cardMetaData: ["scheme": schemeList])) + } + case .failure(let error): + print("Error: \(error.localizedDescription)") + } + } + } else if((state["value"] as! String).count < 8 && calledUpdate) { + calledUpdate = false + self.collectCardNumberElement.update(updateOptions: CollectElementOptions(cardMetaData: ["scheme": []])) + } + } + stackView.addArrangedSubview(collectCardNumberElement!) + + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 5 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2).isActive = true + scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 2).isActive = true + scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + + } + // Sample Bin lookup api call. + func binLookup(bin: String, completion: @escaping (Result<[SkyflowFlowVault.CardType], Error>) -> Void) { + // Set up the request headers + var request = URLRequest(url: URL(string: "https:///v1/card_lookup")!) + request.httpMethod = "POST" + request.addValue("", forHTTPHeaderField: "X-skyflow-authorization") // TODO: replace bearer token + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + // Set up the request body + let requestBody: [String: Any] = ["BIN": bin] + request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody, options: []) + + // Perform the network request1 + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + guard let data = data, let result = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let cardData = result["cards_data"] as? [[String: Any]] else { + completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response"]))) + return + } + // Extract card schemes + let schemeList = self.getCardSchemes(from: cardData) + completion(.success(schemeList)) + } + task.resume() + } + //parse card scheme from the bin api response. + func getCardSchemes(from cardData: [[String: Any]]) -> [SkyflowFlowVault.CardType] { + var schemeList: [SkyflowFlowVault.CardType] = [] + + for card in cardData { + if let cardScheme = card["card_scheme"] as? String { + switch cardScheme { + case "VISA": + schemeList.append(SkyflowFlowVault.CardType.VISA) + case "MASTERCARD": + schemeList.append(SkyflowFlowVault.CardType.MASTERCARD) + case "CARTES BANCAIRES": + schemeList.append(SkyflowFlowVault.CardType.CARTES_BANCAIRES) + default: + break + } + } + } + + return schemeList + } +} + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift new file mode 100644 index 00000000..39890a1b --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import SkyflowFlowVault + +class CollectAndRevealViewController: UIViewController { + var retryCount = 0 + private var skyflow: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var revealContainer: SkyflowFlowVault.Container? + private var b: UIButton? + + private var stackView: UIStackView! + + private var revealCVV: Label? + private var revealCardNumber: Label? + private var revealName: Label? + private var revealExpirationMonth: Label? + private var revealExpirationYear: Label? + private var revealButton: UIButton! + + private var revealed = false + + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + let tokenProvider = ExampleTokenProvider() + + let config = SkyflowFlowVault.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider, + options: SkyflowFlowVault.Options( + logLevel: SkyflowFlowVault.LogLevel.DEBUG + ) + ) + + self.skyflow = SkyflowFlowVault.initialize(config) + + if self.skyflow != nil { + let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COLLECT, options: nil) + self.container = container + self.stackView = UIStackView() + + let baseStyle = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) + + let completedStyle = SkyflowFlowVault.Style(textColor: UIColor.green) + + let invalidStyle = SkyflowFlowVault.Style(textColor: UIColor.red) + + let styles = SkyflowFlowVault.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "card_number", + inputStyles: styles, + label: "Card Number", + placeholder: "4111-1111-1111-1111", + type: SkyflowFlowVault.ElementType.CARD_NUMBER + ) + let collectNameInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "cardholder_name", + inputStyles: styles, + label: "Card Holder Name", + placeholder: "John Doe", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME + ) + let collectCVVInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "cvv", + inputStyles: styles, + label: "CVV", + placeholder: "***", + type: .CVV + ) + let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "expiry_month", + inputStyles: styles, + label: "Expiration Month", + placeholder: "MM", + type: .EXPIRATION_MONTH + ) + let collectExpYearInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "expiry_year", + inputStyles: styles, + label: "Expiration Year", + placeholder: "YYYY", + type: .EXPIRATION_YEAR + ) + let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true, enableCopy: true) + let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) + let collectName = container?.create(input: collectNameInput, options: requiredOption) + let collectCVV = container?.create(input: collectCVVInput, options: requiredOption) + let collectExpMonth = container?.create(input: collectExpMonthInput, options: requiredOption) + let collectExpYear = container?.create(input: collectExpYearInput, options: requiredOption) + let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + collectButton.backgroundColor = .blue + collectButton.setTitle("Submit", for: .normal) + collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + self.revealContainer = skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + self.revealButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + revealButton.backgroundColor = .blue + revealButton.setTitle("Reveal", for: .normal) + revealButton.addTarget(self, action: #selector(revealForm), for: .touchUpInside) + stackView.addArrangedSubview(collectCardNumber!) + stackView.addArrangedSubview(collectName!) + stackView.addArrangedSubview(collectCVV!) + stackView.addArrangedSubview(collectExpMonth!) + stackView.addArrangedSubview(collectExpYear!) + stackView.addArrangedSubview(collectButton) + + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 5 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2).isActive = true + scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 2).isActive = true + scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + @objc func revealForm() { + self.revealContainer?.reveal(callback: ExampleAPICallback()) + } + @objc func submitForm() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + } + internal func updateSuccess(_ response: SuccessResponse) { + print(response) + retryCount = 0 + updateRevealInputs(tokens: response.records[0].fields) + print("Successfully got response:", response) + } + internal func updateFailure(error: Any) { + if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + retryCount += 1 + submitForm() + } + print("Failed Operation", error) + } + internal func updateRevealInputs(tokens: Fields) { + let revealBaseStyle = SkyflowFlowVault.Style( + borderColor: UIColor.black, + cornerRadius: 20, + padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), + borderWidth: 2, + textAlignment: .left, + textColor: UIColor.blue + ) + let revealStyles = SkyflowFlowVault.Styles(base: revealBaseStyle) + DispatchQueue.main.async { + if self.revealed { + self.removeRevealElements() + } else { + self.revealed = true + } + let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( + token: tokens.card_number, + inputStyles: revealStyles, + label: "Card Number", + redaction: .REDACTED + ) + self.revealCardNumber = self.revealContainer?.create( + input: revealCardNumberInput, + options: SkyflowFlowVault.RevealElementOptions() + ) + let revealCVVtInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cvv, + inputStyles: revealStyles, + label: "CVV", + redaction: .MASKED + ) + self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) + let revealNameInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cardholder_name, + inputStyles: revealStyles, + label: "Card Holder Name", + redaction: .DEFAULT + + ) + self.revealName = self.revealContainer?.create(input: revealNameInput) + let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( + token: tokens.expiry_month, + inputStyles: revealStyles, + label: "Expiration Month", + redaction: .PLAIN_TEXT + ) + self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) + let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( + token: tokens.expiry_year, + inputStyles: revealStyles, + label: "Expiration Year" + ) + self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) + self.addRevealElements() + } + } + + internal func removeRevealElements() { + self.stackView.removeArrangedSubview(self.revealCardNumber!) + self.stackView.removeArrangedSubview(self.revealName!) + self.stackView.removeArrangedSubview(self.revealCVV!) + self.stackView.removeArrangedSubview(self.revealExpirationMonth!) + self.stackView.removeArrangedSubview(self.revealExpirationYear!) + self.stackView.removeArrangedSubview(self.revealButton) + self.revealCardNumber?.removeFromSuperview() + self.revealName?.removeFromSuperview() + self.revealCVV?.removeFromSuperview() + self.revealExpirationMonth?.removeFromSuperview() + self.revealExpirationYear?.removeFromSuperview() + self.revealButton.removeFromSuperview() + self.revealContainer = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + } + + internal func addRevealElements() { + DispatchQueue.main.async { + self.stackView.addArrangedSubview(self.revealCardNumber!) + self.stackView.addArrangedSubview(self.revealName!) + self.stackView.addArrangedSubview(self.revealCVV!) + self.stackView.addArrangedSubview(self.revealExpirationMonth!) + self.stackView.addArrangedSubview(self.revealExpirationYear!) + self.stackView.addArrangedSubview(self.revealButton) + } + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift new file mode 100644 index 00000000..cfdf3deb --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleAPICallback: SkyflowFlowVault.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift new file mode 100644 index 00000000..a170d9fe --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist new file mode 100644 index 00000000..acc408d3 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/Info.plist @@ -0,0 +1,71 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift new file mode 100644 index 00000000..959044f5 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let skyflow_id: String +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift new file mode 100644 index 00000000..77fba8c2 --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/SceneDelegate.swift @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift new file mode 100644 index 00000000..e8ee60ce --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ViewController.swift @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import SkyflowFlowVault + +class ViewController: UIViewController { + private var stackView: UIStackView! + + override func viewDidLoad() { + self.stackView = UIStackView() + + let button1 = UIButton() + let button2 = UIButton() + + + view.backgroundColor = .white + + button1.setTitle("Collect And Reveal View", for: .normal) + button1.backgroundColor = .black + button1.setTitleColor(.white, for: .normal) + button1.frame = CGRect(x: 20, y: 70, width: 200, height: 15) + button1.addTarget(self, action: #selector(openCollectAndRevealViewController), for: .touchUpInside) + + button2.setTitle("Open Card Brand Choice Sample", for: .normal) + button2.backgroundColor = .black + button2.setTitleColor(.white, for: .normal) + button2.frame = CGRect(x: 20, y: 70, width: 200, height: 15) + button2.addTarget(self, action: #selector(openCardBrandChoiceSampleViewController), for: .touchUpInside) + + button1.accessibilityIdentifier = "view1" + button2.accessibilityIdentifier = "view2" + + + stackView.addArrangedSubview(button1) + stackView.addArrangedSubview(button2) + + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 2 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 5).isActive = true + scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 10).isActive = true + scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + + } + @objc private func openCollectAndRevealViewController(){ + let rootVC = CollectAndRevealViewController() + rootVC.title = "Card Brand Choice Sample Vie" + let navVC = UINavigationController(rootViewController: rootVC) + navVC.modalPresentationStyle = .fullScreen + + present(navVC, animated: true) + + } + @objc private func openCardBrandChoiceSampleViewController(){ + let rootVC = CardBrandChoiceSampleViewController() + rootVC.title = "Card Brand Choice Sample View" + let navVC = UINavigationController(rootViewController: rootVC) + navVC.modalPresentationStyle = .fullScreen + + present(navVC, animated: true) + + } + +} + diff --git a/Samples/Skyflow/CollectAndRevealSample/Podfile b/SkyflowFlowVault/Samples/CollectAndRevealSample/Podfile similarity index 83% rename from Samples/Skyflow/CollectAndRevealSample/Podfile rename to SkyflowFlowVault/Samples/CollectAndRevealSample/Podfile index ddb60497..5b6855b0 100644 --- a/Samples/Skyflow/CollectAndRevealSample/Podfile +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/Podfile @@ -5,8 +5,9 @@ source 'https://github.com/CocoaPods/Specs.git' target 'CollectAndRevealSample' do #use_frameworks! + pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.25.0" + pod 'SkyflowFlowVault' end diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/README.md b/SkyflowFlowVault/Samples/CollectAndRevealSample/README.md new file mode 100644 index 00000000..93bc512a --- /dev/null +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/README.md @@ -0,0 +1 @@ +This sample illustrates how to use secure Skyflow elements to collect sensitive user information and reveal it to the user via tokens. \ No newline at end of file diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj new file mode 100644 index 00000000..19f868a0 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj @@ -0,0 +1,601 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 7C5748C72A7B71DF002D64AA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748C62A7B71DF002D64AA /* AppDelegate.swift */; }; + 7C5748C92A7B71DF002D64AA /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748C82A7B71DF002D64AA /* SceneDelegate.swift */; }; + 7C5748CB2A7B71DF002D64AA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748CA2A7B71DF002D64AA /* ViewController.swift */; }; + 7C5748CE2A7B71DF002D64AA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C5748CC2A7B71DF002D64AA /* Main.storyboard */; }; + 7C5748D02A7B71DF002D64AA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C5748CF2A7B71DF002D64AA /* Assets.xcassets */; }; + 7C5748D32A7B71DF002D64AA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C5748D12A7B71DF002D64AA /* LaunchScreen.storyboard */; }; + 7C5748DE2A7B71DF002D64AA /* ComposableElementsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748DD2A7B71DF002D64AA /* ComposableElementsTests.swift */; }; + 7C5748E82A7B71DF002D64AA /* ComposableElementsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748E72A7B71DF002D64AA /* ComposableElementsUITests.swift */; }; + 7C5748EA2A7B71DF002D64AA /* ComposableElementsUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748E92A7B71DF002D64AA /* ComposableElementsUITestsLaunchTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7C5748DA2A7B71DF002D64AA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C5748BB2A7B71DF002D64AA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C5748C22A7B71DF002D64AA; + remoteInfo = ComposableElements; + }; + 7C5748E42A7B71DF002D64AA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C5748BB2A7B71DF002D64AA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C5748C22A7B71DF002D64AA; + remoteInfo = ComposableElements; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7C5748C32A7B71DF002D64AA /* ComposableElements.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ComposableElements.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C5748C62A7B71DF002D64AA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7C5748C82A7B71DF002D64AA /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7C5748CA2A7B71DF002D64AA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 7C5748CD2A7B71DF002D64AA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7C5748CF2A7B71DF002D64AA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7C5748D22A7B71DF002D64AA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7C5748D42A7B71DF002D64AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7C5748D92A7B71DF002D64AA /* ComposableElementsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ComposableElementsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C5748DD2A7B71DF002D64AA /* ComposableElementsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposableElementsTests.swift; sourceTree = ""; }; + 7C5748E32A7B71DF002D64AA /* ComposableElementsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ComposableElementsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C5748E72A7B71DF002D64AA /* ComposableElementsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposableElementsUITests.swift; sourceTree = ""; }; + 7C5748E92A7B71DF002D64AA /* ComposableElementsUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposableElementsUITestsLaunchTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7C5748C02A7B71DF002D64AA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C5748D62A7B71DF002D64AA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C5748E02A7B71DF002D64AA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7C5748BA2A7B71DF002D64AA = { + isa = PBXGroup; + children = ( + 7C5748C52A7B71DF002D64AA /* ComposableElements */, + 7C5748DC2A7B71DF002D64AA /* ComposableElementsTests */, + 7C5748E62A7B71DF002D64AA /* ComposableElementsUITests */, + 7C5748C42A7B71DF002D64AA /* Products */, + ); + sourceTree = ""; + }; + 7C5748C42A7B71DF002D64AA /* Products */ = { + isa = PBXGroup; + children = ( + 7C5748C32A7B71DF002D64AA /* ComposableElements.app */, + 7C5748D92A7B71DF002D64AA /* ComposableElementsTests.xctest */, + 7C5748E32A7B71DF002D64AA /* ComposableElementsUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7C5748C52A7B71DF002D64AA /* ComposableElements */ = { + isa = PBXGroup; + children = ( + 7C5748C62A7B71DF002D64AA /* AppDelegate.swift */, + 7C5748C82A7B71DF002D64AA /* SceneDelegate.swift */, + 7C5748CA2A7B71DF002D64AA /* ViewController.swift */, + 7C5748CC2A7B71DF002D64AA /* Main.storyboard */, + 7C5748CF2A7B71DF002D64AA /* Assets.xcassets */, + 7C5748D12A7B71DF002D64AA /* LaunchScreen.storyboard */, + 7C5748D42A7B71DF002D64AA /* Info.plist */, + ); + path = ComposableElements; + sourceTree = ""; + }; + 7C5748DC2A7B71DF002D64AA /* ComposableElementsTests */ = { + isa = PBXGroup; + children = ( + 7C5748DD2A7B71DF002D64AA /* ComposableElementsTests.swift */, + ); + path = ComposableElementsTests; + sourceTree = ""; + }; + 7C5748E62A7B71DF002D64AA /* ComposableElementsUITests */ = { + isa = PBXGroup; + children = ( + 7C5748E72A7B71DF002D64AA /* ComposableElementsUITests.swift */, + 7C5748E92A7B71DF002D64AA /* ComposableElementsUITestsLaunchTests.swift */, + ); + path = ComposableElementsUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7C5748C22A7B71DF002D64AA /* ComposableElements */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C5748ED2A7B71DF002D64AA /* Build configuration list for PBXNativeTarget "ComposableElements" */; + buildPhases = ( + 7C5748BF2A7B71DF002D64AA /* Sources */, + 7C5748C02A7B71DF002D64AA /* Frameworks */, + 7C5748C12A7B71DF002D64AA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ComposableElements; + productName = ComposableElements; + productReference = 7C5748C32A7B71DF002D64AA /* ComposableElements.app */; + productType = "com.apple.product-type.application"; + }; + 7C5748D82A7B71DF002D64AA /* ComposableElementsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C5748F02A7B71DF002D64AA /* Build configuration list for PBXNativeTarget "ComposableElementsTests" */; + buildPhases = ( + 7C5748D52A7B71DF002D64AA /* Sources */, + 7C5748D62A7B71DF002D64AA /* Frameworks */, + 7C5748D72A7B71DF002D64AA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7C5748DB2A7B71DF002D64AA /* PBXTargetDependency */, + ); + name = ComposableElementsTests; + productName = ComposableElementsTests; + productReference = 7C5748D92A7B71DF002D64AA /* ComposableElementsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7C5748E22A7B71DF002D64AA /* ComposableElementsUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C5748F32A7B71DF002D64AA /* Build configuration list for PBXNativeTarget "ComposableElementsUITests" */; + buildPhases = ( + 7C5748DF2A7B71DF002D64AA /* Sources */, + 7C5748E02A7B71DF002D64AA /* Frameworks */, + 7C5748E12A7B71DF002D64AA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7C5748E52A7B71DF002D64AA /* PBXTargetDependency */, + ); + name = ComposableElementsUITests; + productName = ComposableElementsUITests; + productReference = 7C5748E32A7B71DF002D64AA /* ComposableElementsUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7C5748BB2A7B71DF002D64AA /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 7C5748C22A7B71DF002D64AA = { + CreatedOnToolsVersion = 14.0; + }; + 7C5748D82A7B71DF002D64AA = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 7C5748C22A7B71DF002D64AA; + }; + 7C5748E22A7B71DF002D64AA = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 7C5748C22A7B71DF002D64AA; + }; + }; + }; + buildConfigurationList = 7C5748BE2A7B71DF002D64AA /* Build configuration list for PBXProject "ComposableElements" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7C5748BA2A7B71DF002D64AA; + productRefGroup = 7C5748C42A7B71DF002D64AA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7C5748C22A7B71DF002D64AA /* ComposableElements */, + 7C5748D82A7B71DF002D64AA /* ComposableElementsTests */, + 7C5748E22A7B71DF002D64AA /* ComposableElementsUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7C5748C12A7B71DF002D64AA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C5748D32A7B71DF002D64AA /* LaunchScreen.storyboard in Resources */, + 7C5748D02A7B71DF002D64AA /* Assets.xcassets in Resources */, + 7C5748CE2A7B71DF002D64AA /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C5748D72A7B71DF002D64AA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C5748E12A7B71DF002D64AA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7C5748BF2A7B71DF002D64AA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C5748CB2A7B71DF002D64AA /* ViewController.swift in Sources */, + 7C5748C72A7B71DF002D64AA /* AppDelegate.swift in Sources */, + 7C5748C92A7B71DF002D64AA /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C5748D52A7B71DF002D64AA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C5748DE2A7B71DF002D64AA /* ComposableElementsTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C5748DF2A7B71DF002D64AA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C5748EA2A7B71DF002D64AA /* ComposableElementsUITestsLaunchTests.swift in Sources */, + 7C5748E82A7B71DF002D64AA /* ComposableElementsUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7C5748DB2A7B71DF002D64AA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C5748C22A7B71DF002D64AA /* ComposableElements */; + targetProxy = 7C5748DA2A7B71DF002D64AA /* PBXContainerItemProxy */; + }; + 7C5748E52A7B71DF002D64AA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C5748C22A7B71DF002D64AA /* ComposableElements */; + targetProxy = 7C5748E42A7B71DF002D64AA /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 7C5748CC2A7B71DF002D64AA /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C5748CD2A7B71DF002D64AA /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 7C5748D12A7B71DF002D64AA /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C5748D22A7B71DF002D64AA /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7C5748EB2A7B71DF002D64AA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7C5748EC2A7B71DF002D64AA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7C5748EE2A7B71DF002D64AA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ComposableElements/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.ComposableElements; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7C5748EF2A7B71DF002D64AA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ComposableElements/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.ComposableElements; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7C5748F12A7B71DF002D64AA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.ComposableElementsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ComposableElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ComposableElements"; + }; + name = Debug; + }; + 7C5748F22A7B71DF002D64AA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.ComposableElementsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ComposableElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ComposableElements"; + }; + name = Release; + }; + 7C5748F42A7B71DF002D64AA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.ComposableElementsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ComposableElements; + }; + name = Debug; + }; + 7C5748F52A7B71DF002D64AA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.ComposableElementsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ComposableElements; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7C5748BE2A7B71DF002D64AA /* Build configuration list for PBXProject "ComposableElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C5748EB2A7B71DF002D64AA /* Debug */, + 7C5748EC2A7B71DF002D64AA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C5748ED2A7B71DF002D64AA /* Build configuration list for PBXNativeTarget "ComposableElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C5748EE2A7B71DF002D64AA /* Debug */, + 7C5748EF2A7B71DF002D64AA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C5748F02A7B71DF002D64AA /* Build configuration list for PBXNativeTarget "ComposableElementsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C5748F12A7B71DF002D64AA /* Debug */, + 7C5748F22A7B71DF002D64AA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C5748F32A7B71DF002D64AA /* Build configuration list for PBXNativeTarget "ComposableElementsUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C5748F42A7B71DF002D64AA /* Debug */, + 7C5748F52A7B71DF002D64AA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7C5748BB2A7B71DF002D64AA /* Project object */; +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/AppDelegate.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/AppDelegate.swift new file mode 100644 index 00000000..9199b13e --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// ComposableElements +// +// Created by Bharti Sagar on 03/08/23. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift new file mode 100644 index 00000000..cfdf3deb --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleAPICallback: SkyflowFlowVault.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift new file mode 100644 index 00000000..a170d9fe --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Info.plist b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift new file mode 100644 index 00000000..959044f5 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let skyflow_id: String +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/SceneDelegate.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/SceneDelegate.swift new file mode 100644 index 00000000..1a1f3f03 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// ComposableElements +// +// Created by Bharti Sagar on 03/08/23. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Untitled.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/Untitled.swift new file mode 100644 index 00000000..e69de29b diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift new file mode 100644 index 00000000..cf71db43 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import SkyflowFlowVault + +class ViewController: UIViewController { + var retryCount = 0 + private var skyflow: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var revealContainer: SkyflowFlowVault.Container? + private var b: UIButton? + + private var stackView: UIStackView! + + private var revealCVV: Label? + private var revealCardNumber: Label? + private var revealName: Label? + private var revealExpirationMonth: Label? + private var revealExpirationYear: Label? + private var revealButton: UIButton! + + private var revealed = false + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + let tokenProvider = ExampleTokenProvider() + + let config = SkyflowFlowVault.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider, + options: SkyflowFlowVault.Options( + logLevel: SkyflowFlowVault.LogLevel.DEBUG + ) + ) + + self.skyflow = SkyflowFlowVault.initialize(config) + + if self.skyflow != nil { + let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1,2,2], styles: Styles(base: Style(borderColor: UIColor.gray) ), errorTextStyles: Styles(base: Style(textColor: UIColor.red) ))) + self.container = container + self.stackView = UIStackView() + + let baseStyle = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) + + let completedStyle = SkyflowFlowVault.Style(textColor: UIColor.green) + + let invalidStyle = SkyflowFlowVault.Style(textColor: UIColor.red) + + let styles = SkyflowFlowVault.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "card_number", + inputStyles: styles, + label: "Card Number", + placeholder: "4111-1111-1111-1111", + type: SkyflowFlowVault.ElementType.CARD_NUMBER + ) + let collectNameInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "cardholder_name", + inputStyles: styles, + label: "Card Holder Name", + placeholder: "John Doe", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME + ) + let collectCVVInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "cvv", + inputStyles: styles, + label: "CVV", + placeholder: "***", + type: .CVV + ) + let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "expiry_month", + inputStyles: styles, + label: "Expiration Month", + placeholder: "MM", + type: .EXPIRATION_MONTH + ) + let collectExpYearInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "expiry_year", + inputStyles: styles, + label: "Expiration Year", + placeholder: "YYYY", + type: .EXPIRATION_YEAR + ) + let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) + let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) + let collectName = container?.create(input: collectNameInput, options: requiredOption) + let collectCVV = container?.create(input: collectCVVInput, options: requiredOption) + let collectExpMonth = container?.create(input: collectExpMonthInput, options: requiredOption) + let collectExpYear = container?.create(input: collectExpYearInput, options: requiredOption) + let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + collectButton.backgroundColor = .blue + collectButton.setTitle("Submit", for: .normal) + collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + self.revealContainer = skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + self.revealButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + revealButton.backgroundColor = .blue + revealButton.setTitle("Reveal", for: .normal) + revealButton.addTarget(self, action: #selector(revealForm), for: .touchUpInside) + + do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) + } catch { + print(error) + } + + stackView.addArrangedSubview(collectButton) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.backgroundColor = .white + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 20 + ).isActive = true + scrollView.leftAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leftAnchor, + constant: 10 + ).isActive = true + scrollView.rightAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.rightAnchor, + constant: -10 + ).isActive = true + scrollView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor + ).isActive = true + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + @objc func revealForm() { + self.revealContainer?.reveal(callback: ExampleAPICallback()) + } + @objc func submitForm() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + } + internal func updateSuccess(_ response: SuccessResponse) { + print(response) + retryCount = 0 + updateRevealInputs(tokens: response.records[0].fields) + print("Successfully got response:", response) + } + internal func updateFailure(error: Any) { + if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + retryCount += 1 + submitForm() + } + print("Failed Operation", error) + } + internal func updateRevealInputs(tokens: Fields) { + let revealBaseStyle = SkyflowFlowVault.Style( + borderColor: UIColor.black, + cornerRadius: 20, + padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), + borderWidth: 2, + textAlignment: .left, + textColor: UIColor.blue + ) + let revealStyles = SkyflowFlowVault.Styles(base: revealBaseStyle) + DispatchQueue.main.async { + if self.revealed { + self.removeRevealElements() + } else { + self.revealed = true + } + let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( + token: tokens.card_number, + inputStyles: revealStyles, + label: "Card Number", + redaction: .REDACTED + ) + self.revealCardNumber = self.revealContainer?.create( + input: revealCardNumberInput, + options: SkyflowFlowVault.RevealElementOptions() + ) + let revealCVVtInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cvv, + inputStyles: revealStyles, + label: "CVV", + redaction: .MASKED + ) + self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) + let revealNameInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cardholder_name, + inputStyles: revealStyles, + label: "Card Holder Name", + redaction: .DEFAULT + + ) + self.revealName = self.revealContainer?.create(input: revealNameInput) + let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( + token: tokens.expiry_month, + inputStyles: revealStyles, + label: "Expiration Month", + redaction: .PLAIN_TEXT + ) + self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) + let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( + token: tokens.expiry_year, + inputStyles: revealStyles, + label: "Expiration Year" + ) + self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) + self.addRevealElements() + } + } + + internal func removeRevealElements() { + self.stackView.removeArrangedSubview(self.revealCardNumber!) + self.stackView.removeArrangedSubview(self.revealName!) + self.stackView.removeArrangedSubview(self.revealCVV!) + self.stackView.removeArrangedSubview(self.revealExpirationMonth!) + self.stackView.removeArrangedSubview(self.revealExpirationYear!) + self.stackView.removeArrangedSubview(self.revealButton) + self.revealCardNumber?.removeFromSuperview() + self.revealName?.removeFromSuperview() + self.revealCVV?.removeFromSuperview() + self.revealExpirationMonth?.removeFromSuperview() + self.revealExpirationYear?.removeFromSuperview() + self.revealButton.removeFromSuperview() + self.revealContainer = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + } + + internal func addRevealElements() { + DispatchQueue.main.async { + self.stackView.addArrangedSubview(self.revealCardNumber!) + self.stackView.addArrangedSubview(self.revealName!) + self.stackView.addArrangedSubview(self.revealCVV!) + self.stackView.addArrangedSubview(self.revealExpirationMonth!) + self.stackView.addArrangedSubview(self.revealExpirationYear!) + self.stackView.addArrangedSubview(self.revealButton) + } + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift new file mode 100644 index 00000000..03b8ffb0 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsTests/ComposableElementsTests.swift @@ -0,0 +1,36 @@ +// +// ComposableElementsTests.swift +// ComposableElementsTests +// +// Created by Bharti Sagar on 03/08/23. +// + +import XCTest +@testable import ComposableElements + +final class ComposableElementsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift new file mode 100644 index 00000000..97bee556 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITests.swift @@ -0,0 +1,41 @@ +// +// ComposableElementsUITests.swift +// ComposableElementsUITests +// +// Created by Bharti Sagar on 03/08/23. +// + +import XCTest + +final class ComposableElementsUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift new file mode 100644 index 00000000..72c91621 --- /dev/null +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElementsUITests/ComposableElementsUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// ComposableElementsUITestsLaunchTests.swift +// ComposableElementsUITests +// +// Created by Bharti Sagar on 03/08/23. +// + +import XCTest + +final class ComposableElementsUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Samples/Skyflow/ComposableElements/Podfile b/SkyflowFlowVault/Samples/ComposableElements/Podfile similarity index 89% rename from Samples/Skyflow/ComposableElements/Podfile rename to SkyflowFlowVault/Samples/ComposableElements/Podfile index 77ef17f8..2e862ce5 100644 --- a/Samples/Skyflow/ComposableElements/Podfile +++ b/SkyflowFlowVault/Samples/ComposableElements/Podfile @@ -4,7 +4,8 @@ target 'ComposableElements' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'Skyflow', ">= 1.25.0" + pod 'AEXML', '4.6.1' + pod 'SkyflowFlowVault' # Pods for ComposableElements target 'ComposableElementsTests' do diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj new file mode 100644 index 00000000..6605649c --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj @@ -0,0 +1,779 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 3001626AF634014586294C89 /* Pods_InputFormattingTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D28B0C4C19EA3040136B35C5 /* Pods_InputFormattingTests.framework */; }; + 7C4197A629F978A8002CA73E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197A529F978A8002CA73E /* AppDelegate.swift */; }; + 7C4197A829F978A8002CA73E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197A729F978A8002CA73E /* SceneDelegate.swift */; }; + 7C4197AA29F978A8002CA73E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197A929F978A8002CA73E /* ViewController.swift */; }; + 7C4197AD29F978A8002CA73E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C4197AB29F978A8002CA73E /* Main.storyboard */; }; + 7C4197AF29F978A9002CA73E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C4197AE29F978A9002CA73E /* Assets.xcassets */; }; + 7C4197B229F978A9002CA73E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C4197B029F978A9002CA73E /* LaunchScreen.storyboard */; }; + 7C4197BD29F978A9002CA73E /* InputFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197BC29F978A9002CA73E /* InputFormattingTests.swift */; }; + 7C4197C729F978A9002CA73E /* InputFormattingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197C629F978A9002CA73E /* InputFormattingUITests.swift */; }; + 7C4197C929F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197C829F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift */; }; + 7C4197D629F97E9F002CA73E /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */; }; + 7C4197D729F97E9F002CA73E /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */; }; + 7C4197D829F97E9F002CA73E /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */; }; + 7C4197DA29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */; }; + 7C4197DB29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */; }; + 7C4197DC29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */; }; + 7C4197DE29F97EFD002CA73E /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */; }; + 7C4197DF29F97EFD002CA73E /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */; }; + 7C4197E029F97EFD002CA73E /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */; }; + 8E4FC698E4F6F8422455BB76 /* Pods_InputFormatting_InputFormattingUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 362238A2034F5E696B5CB492 /* Pods_InputFormatting_InputFormattingUITests.framework */; }; + 96007AFE98552781CD9CA0FB /* Pods_InputFormatting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15DB42C2155F11A364E2EA97 /* Pods_InputFormatting.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7C4197B929F978A9002CA73E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C41979A29F978A8002CA73E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C4197A129F978A8002CA73E; + remoteInfo = InputFormatting; + }; + 7C4197C329F978A9002CA73E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7C41979A29F978A8002CA73E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C4197A129F978A8002CA73E; + remoteInfo = InputFormatting; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 15DB42C2155F11A364E2EA97 /* Pods_InputFormatting.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InputFormatting.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 188CA6653F94D96CEF0AF393 /* Pods-InputFormatting-InputFormattingUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormatting-InputFormattingUITests.release.xcconfig"; path = "Target Support Files/Pods-InputFormatting-InputFormattingUITests/Pods-InputFormatting-InputFormattingUITests.release.xcconfig"; sourceTree = ""; }; + 29A0E1E3BEB0B980E0EE2197 /* Pods-InputFormattingTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormattingTests.debug.xcconfig"; path = "Target Support Files/Pods-InputFormattingTests/Pods-InputFormattingTests.debug.xcconfig"; sourceTree = ""; }; + 362238A2034F5E696B5CB492 /* Pods_InputFormatting_InputFormattingUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InputFormatting_InputFormattingUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 65129EEDB0F8F71FDAB38277 /* Pods-InputFormatting-InputFormattingUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormatting-InputFormattingUITests.debug.xcconfig"; path = "Target Support Files/Pods-InputFormatting-InputFormattingUITests/Pods-InputFormatting-InputFormattingUITests.debug.xcconfig"; sourceTree = ""; }; + 7C4197A229F978A8002CA73E /* InputFormatting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InputFormatting.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C4197A529F978A8002CA73E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7C4197A729F978A8002CA73E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7C4197A929F978A8002CA73E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 7C4197AC29F978A8002CA73E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7C4197AE29F978A9002CA73E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7C4197B129F978A9002CA73E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7C4197B329F978A9002CA73E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7C4197B829F978A9002CA73E /* InputFormattingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InputFormattingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C4197BC29F978A9002CA73E /* InputFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFormattingTests.swift; sourceTree = ""; }; + 7C4197C229F978A9002CA73E /* InputFormattingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InputFormattingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C4197C629F978A9002CA73E /* InputFormattingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFormattingUITests.swift; sourceTree = ""; }; + 7C4197C829F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFormattingUITestsLaunchTests.swift; sourceTree = ""; }; + 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; + 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; + 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; + B4544E89336067DF84CD3C85 /* Pods-InputFormatting.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormatting.debug.xcconfig"; path = "Target Support Files/Pods-InputFormatting/Pods-InputFormatting.debug.xcconfig"; sourceTree = ""; }; + BD7E5EBA615A5DD2B93E86DB /* Pods-InputFormattingTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormattingTests.release.xcconfig"; path = "Target Support Files/Pods-InputFormattingTests/Pods-InputFormattingTests.release.xcconfig"; sourceTree = ""; }; + D28B0C4C19EA3040136B35C5 /* Pods_InputFormattingTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InputFormattingTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F5CF4C3281D68DEDF8999D73 /* Pods-InputFormatting.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormatting.release.xcconfig"; path = "Target Support Files/Pods-InputFormatting/Pods-InputFormatting.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7C41979F29F978A8002CA73E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 96007AFE98552781CD9CA0FB /* Pods_InputFormatting.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C4197B529F978A9002CA73E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3001626AF634014586294C89 /* Pods_InputFormattingTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C4197BF29F978A9002CA73E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8E4FC698E4F6F8422455BB76 /* Pods_InputFormatting_InputFormattingUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7C41979929F978A8002CA73E = { + isa = PBXGroup; + children = ( + 7C4197A429F978A8002CA73E /* InputFormatting */, + 7C4197BB29F978A9002CA73E /* InputFormattingTests */, + 7C4197C529F978A9002CA73E /* InputFormattingUITests */, + 7C4197A329F978A8002CA73E /* Products */, + AC9CAFF170378F6E80243B57 /* Pods */, + F536FDA0525A192FE5AF7E7E /* Frameworks */, + ); + sourceTree = ""; + }; + 7C4197A329F978A8002CA73E /* Products */ = { + isa = PBXGroup; + children = ( + 7C4197A229F978A8002CA73E /* InputFormatting.app */, + 7C4197B829F978A9002CA73E /* InputFormattingTests.xctest */, + 7C4197C229F978A9002CA73E /* InputFormattingUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7C4197A429F978A8002CA73E /* InputFormatting */ = { + isa = PBXGroup; + children = ( + 7C4197A529F978A8002CA73E /* AppDelegate.swift */, + 7C4197A729F978A8002CA73E /* SceneDelegate.swift */, + 7C4197A929F978A8002CA73E /* ViewController.swift */, + 7C4197AB29F978A8002CA73E /* Main.storyboard */, + 7C4197AE29F978A9002CA73E /* Assets.xcassets */, + 7C4197B029F978A9002CA73E /* LaunchScreen.storyboard */, + 7C4197B329F978A9002CA73E /* Info.plist */, + 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */, + 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */, + 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */, + ); + path = InputFormatting; + sourceTree = ""; + }; + 7C4197BB29F978A9002CA73E /* InputFormattingTests */ = { + isa = PBXGroup; + children = ( + 7C4197BC29F978A9002CA73E /* InputFormattingTests.swift */, + ); + path = InputFormattingTests; + sourceTree = ""; + }; + 7C4197C529F978A9002CA73E /* InputFormattingUITests */ = { + isa = PBXGroup; + children = ( + 7C4197C629F978A9002CA73E /* InputFormattingUITests.swift */, + 7C4197C829F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift */, + ); + path = InputFormattingUITests; + sourceTree = ""; + }; + AC9CAFF170378F6E80243B57 /* Pods */ = { + isa = PBXGroup; + children = ( + B4544E89336067DF84CD3C85 /* Pods-InputFormatting.debug.xcconfig */, + F5CF4C3281D68DEDF8999D73 /* Pods-InputFormatting.release.xcconfig */, + 65129EEDB0F8F71FDAB38277 /* Pods-InputFormatting-InputFormattingUITests.debug.xcconfig */, + 188CA6653F94D96CEF0AF393 /* Pods-InputFormatting-InputFormattingUITests.release.xcconfig */, + 29A0E1E3BEB0B980E0EE2197 /* Pods-InputFormattingTests.debug.xcconfig */, + BD7E5EBA615A5DD2B93E86DB /* Pods-InputFormattingTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + F536FDA0525A192FE5AF7E7E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 15DB42C2155F11A364E2EA97 /* Pods_InputFormatting.framework */, + 362238A2034F5E696B5CB492 /* Pods_InputFormatting_InputFormattingUITests.framework */, + D28B0C4C19EA3040136B35C5 /* Pods_InputFormattingTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7C4197A129F978A8002CA73E /* InputFormatting */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C4197CC29F978A9002CA73E /* Build configuration list for PBXNativeTarget "InputFormatting" */; + buildPhases = ( + EAE655EC29BB8E025EAB6ACD /* [CP] Check Pods Manifest.lock */, + 7C41979E29F978A8002CA73E /* Sources */, + 7C41979F29F978A8002CA73E /* Frameworks */, + 7C4197A029F978A8002CA73E /* Resources */, + A05647D53546F65402357FF1 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InputFormatting; + productName = InputFormatting; + productReference = 7C4197A229F978A8002CA73E /* InputFormatting.app */; + productType = "com.apple.product-type.application"; + }; + 7C4197B729F978A9002CA73E /* InputFormattingTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C4197CF29F978A9002CA73E /* Build configuration list for PBXNativeTarget "InputFormattingTests" */; + buildPhases = ( + 167316BE108604695C993807 /* [CP] Check Pods Manifest.lock */, + 7C4197B429F978A9002CA73E /* Sources */, + 7C4197B529F978A9002CA73E /* Frameworks */, + 7C4197B629F978A9002CA73E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7C4197BA29F978A9002CA73E /* PBXTargetDependency */, + ); + name = InputFormattingTests; + productName = InputFormattingTests; + productReference = 7C4197B829F978A9002CA73E /* InputFormattingTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7C4197C129F978A9002CA73E /* InputFormattingUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C4197D229F978A9002CA73E /* Build configuration list for PBXNativeTarget "InputFormattingUITests" */; + buildPhases = ( + AC350B8DC725FBF3E15E1296 /* [CP] Check Pods Manifest.lock */, + 7C4197BE29F978A9002CA73E /* Sources */, + 7C4197BF29F978A9002CA73E /* Frameworks */, + 7C4197C029F978A9002CA73E /* Resources */, + D68982E9DC6BEC48BA5E3C3C /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 7C4197C429F978A9002CA73E /* PBXTargetDependency */, + ); + name = InputFormattingUITests; + productName = InputFormattingUITests; + productReference = 7C4197C229F978A9002CA73E /* InputFormattingUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7C41979A29F978A8002CA73E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 7C4197A129F978A8002CA73E = { + CreatedOnToolsVersion = 14.0; + }; + 7C4197B729F978A9002CA73E = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 7C4197A129F978A8002CA73E; + }; + 7C4197C129F978A9002CA73E = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 7C4197A129F978A8002CA73E; + }; + }; + }; + buildConfigurationList = 7C41979D29F978A8002CA73E /* Build configuration list for PBXProject "InputFormatting" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7C41979929F978A8002CA73E; + productRefGroup = 7C4197A329F978A8002CA73E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7C4197A129F978A8002CA73E /* InputFormatting */, + 7C4197B729F978A9002CA73E /* InputFormattingTests */, + 7C4197C129F978A9002CA73E /* InputFormattingUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7C4197A029F978A8002CA73E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C4197B229F978A9002CA73E /* LaunchScreen.storyboard in Resources */, + 7C4197AF29F978A9002CA73E /* Assets.xcassets in Resources */, + 7C4197AD29F978A8002CA73E /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C4197B629F978A9002CA73E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C4197C029F978A9002CA73E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 167316BE108604695C993807 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InputFormattingTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A05647D53546F65402357FF1 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-InputFormatting/Pods-InputFormatting-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-InputFormatting/Pods-InputFormatting-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-InputFormatting/Pods-InputFormatting-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AC350B8DC725FBF3E15E1296 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InputFormatting-InputFormattingUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D68982E9DC6BEC48BA5E3C3C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-InputFormatting-InputFormattingUITests/Pods-InputFormatting-InputFormattingUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-InputFormatting-InputFormattingUITests/Pods-InputFormatting-InputFormattingUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-InputFormatting-InputFormattingUITests/Pods-InputFormatting-InputFormattingUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EAE655EC29BB8E025EAB6ACD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InputFormatting-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7C41979E29F978A8002CA73E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C4197AA29F978A8002CA73E /* ViewController.swift in Sources */, + 7C4197DE29F97EFD002CA73E /* ResponseStructs.swift in Sources */, + 7C4197DA29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */, + 7C4197A629F978A8002CA73E /* AppDelegate.swift in Sources */, + 7C4197D629F97E9F002CA73E /* ExampleAPICallback.swift in Sources */, + 7C4197A829F978A8002CA73E /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C4197B429F978A9002CA73E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C4197D729F97E9F002CA73E /* ExampleAPICallback.swift in Sources */, + 7C4197DB29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */, + 7C4197BD29F978A9002CA73E /* InputFormattingTests.swift in Sources */, + 7C4197DF29F97EFD002CA73E /* ResponseStructs.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C4197BE29F978A9002CA73E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C4197E029F97EFD002CA73E /* ResponseStructs.swift in Sources */, + 7C4197C929F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift in Sources */, + 7C4197DC29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */, + 7C4197D829F97E9F002CA73E /* ExampleAPICallback.swift in Sources */, + 7C4197C729F978A9002CA73E /* InputFormattingUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7C4197BA29F978A9002CA73E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C4197A129F978A8002CA73E /* InputFormatting */; + targetProxy = 7C4197B929F978A9002CA73E /* PBXContainerItemProxy */; + }; + 7C4197C429F978A9002CA73E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C4197A129F978A8002CA73E /* InputFormatting */; + targetProxy = 7C4197C329F978A9002CA73E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 7C4197AB29F978A8002CA73E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C4197AC29F978A8002CA73E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 7C4197B029F978A9002CA73E /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C4197B129F978A9002CA73E /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7C4197CA29F978A9002CA73E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7C4197CB29F978A9002CA73E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7C4197CD29F978A9002CA73E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B4544E89336067DF84CD3C85 /* Pods-InputFormatting.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = InputFormatting/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7C4197CE29F978A9002CA73E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F5CF4C3281D68DEDF8999D73 /* Pods-InputFormatting.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = InputFormatting/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7C4197D029F978A9002CA73E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 29A0E1E3BEB0B980E0EE2197 /* Pods-InputFormattingTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.InputFormattingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InputFormatting.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/InputFormatting"; + }; + name = Debug; + }; + 7C4197D129F978A9002CA73E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BD7E5EBA615A5DD2B93E86DB /* Pods-InputFormattingTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.InputFormattingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InputFormatting.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/InputFormatting"; + }; + name = Release; + }; + 7C4197D329F978A9002CA73E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 65129EEDB0F8F71FDAB38277 /* Pods-InputFormatting-InputFormattingUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.InputFormattingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = InputFormatting; + }; + name = Debug; + }; + 7C4197D429F978A9002CA73E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 188CA6653F94D96CEF0AF393 /* Pods-InputFormatting-InputFormattingUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.InputFormattingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = InputFormatting; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7C41979D29F978A8002CA73E /* Build configuration list for PBXProject "InputFormatting" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C4197CA29F978A9002CA73E /* Debug */, + 7C4197CB29F978A9002CA73E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C4197CC29F978A9002CA73E /* Build configuration list for PBXNativeTarget "InputFormatting" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C4197CD29F978A9002CA73E /* Debug */, + 7C4197CE29F978A9002CA73E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C4197CF29F978A9002CA73E /* Build configuration list for PBXNativeTarget "InputFormattingTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C4197D029F978A9002CA73E /* Debug */, + 7C4197D129F978A9002CA73E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C4197D229F978A9002CA73E /* Build configuration list for PBXNativeTarget "InputFormattingUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C4197D329F978A9002CA73E /* Debug */, + 7C4197D429F978A9002CA73E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7C41979A29F978A8002CA73E /* Project object */; +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..ca1b3566 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/AppDelegate.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/AppDelegate.swift new file mode 100644 index 00000000..cba9a64b --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/AppDelegate.swift @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// Created by Bharti Sagar on 26/04/23. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift new file mode 100644 index 00000000..7cf1c0a0 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Skyflow + */ +// Created by Bharti Sagar on 26/04/23. +// + +import Foundation +import SkyflowFlowVault + +public class ExampleAPICallback: SkyflowFlowVault.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift new file mode 100644 index 00000000..3c9094ec --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleTokenProvider.swift @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// Created by Bharti Sagar on 26/04/23. +// + +import Foundation +import SkyflowFlowVault + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Info.plist b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift new file mode 100644 index 00000000..2230b92f --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// Created by Bharti Sagar on 26/04/23. +// + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardholder_name: String + let card_number: String + let expiry_month: String + let expiry_year: String + let cvv: String + let ssn: String + let phone_number: String + let license_number: String + let skyflow_id: String +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/SceneDelegate.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/SceneDelegate.swift new file mode 100644 index 00000000..63b04d6e --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/SceneDelegate.swift @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Skyflow + */ +// Created by Bharti Sagar on 26/04/23. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift new file mode 100644 index 00000000..0616287e --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2022 Skyflow + */ +// Created by Bharti Sagar on 26/04/23. +// + +import UIKit +import SkyflowFlowVault + +class ViewController: UIViewController { + private var skyflow: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var revealContainer: SkyflowFlowVault.Container? + private var b: UIButton? + + private var stackView: UIStackView! + + private var revealCVV: Label? + private var revealCardNumber: Label? + private var revealName: Label? + private var revealExpirationMonth: Label? + private var revealExpirationYear: Label? + private var revealSSN: Label? + private var revealPhoneNumber: Label? + private var revealLicenseNumber: Label? + private var revealButton: UIButton! + + private var revealed = false + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + let tokenProvider = ExampleTokenProvider() + + let config = SkyflowFlowVault.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider, + options: SkyflowFlowVault.Options( + logLevel: SkyflowFlowVault.LogLevel.DEBUG + ) + ) + + self.skyflow = SkyflowFlowVault.initialize(config) + + if self.skyflow != nil { + let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COLLECT, options: nil) + self.container = container + self.stackView = UIStackView() + + let baseStyle = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) + + let completedStyle = SkyflowFlowVault.Style(textColor: UIColor.green) + + let invalidStyle = SkyflowFlowVault.Style(textColor: UIColor.red) + + let styles = SkyflowFlowVault.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "card_number", + inputStyles: styles, + label: "Card Number", + placeholder: "4111-1111-1111-1111", + type: SkyflowFlowVault.ElementType.CARD_NUMBER + ) + let collectNameInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "cardholder_name", + inputStyles: styles, + label: "Card Holder Name", + placeholder: "John Doe", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME + ) + let collectCVVInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "cvv", + inputStyles: styles, + label: "CVV", + placeholder: "***", + type: .CVV + ) + let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "expiry_month", + inputStyles: styles, + label: "Expiration Month", + placeholder: "MM", + type: .EXPIRATION_MONTH + ) + let collectExpYearInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "expiry_year", + inputStyles: styles, + label: "Expiration Year", + placeholder: "YYYY", + type: .EXPIRATION_YEAR + ) + let collectSSNInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "ssn", + inputStyles: styles, + label: "SSN", + placeholder: "XXX-XX-XXXX", + type: .INPUT_FIELD + ) + let collectPhoneNumberInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "phone_number", + inputStyles: styles, + label: "Phone Number", + placeholder: "+91 XXXX-XX-XXXX", + type: .INPUT_FIELD + ) + let collectLicenseNumberInput = SkyflowFlowVault.CollectElementInput( + table: "table", + column: "license_number", + inputStyles: styles, + label: "License Number", + placeholder: "X YYY YYY YY YY", + type: .INPUT_FIELD + ) + let collectCardNumber = container?.create(input: collectCardNumberInput, options: SkyflowFlowVault.CollectElementOptions(required: true, format: "XXXX-XXXX-XXXX-XXXX")) + let collectName = container?.create(input: collectNameInput, options: SkyflowFlowVault.CollectElementOptions(required: true)) + let collectCVV = container?.create(input: collectCVVInput, options: SkyflowFlowVault.CollectElementOptions(required: true)) + let collectExpMonth = container?.create(input: collectExpMonthInput, options: SkyflowFlowVault.CollectElementOptions(required: true)) + let collectExpYear = container?.create(input: collectExpYearInput, options: SkyflowFlowVault.CollectElementOptions(required: true)) + let collectSSN = container?.create(input: collectSSNInput, options: SkyflowFlowVault.CollectElementOptions(required: true, format: "XXX-XX-XXXX", translation: ["X": "[0-9]"])) + let collectPhoneNumber = container?.create(input: collectPhoneNumberInput, options: SkyflowFlowVault.CollectElementOptions(required: true, format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) + let collectLicenseNumber = container?.create(input: collectLicenseNumberInput, options: SkyflowFlowVault.CollectElementOptions(required: true, format: "X YYY YYY YY YY", translation: ["X": "[A-Z]","Y": "[0-9]"])) + + let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + collectButton.backgroundColor = .blue + collectButton.setTitle("Submit", for: .normal) + collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + self.revealContainer = skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + self.revealButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + revealButton.backgroundColor = .blue + revealButton.setTitle("Reveal", for: .normal) + revealButton.addTarget(self, action: #selector(revealForm), for: .touchUpInside) + stackView.addArrangedSubview(collectCardNumber!) + stackView.addArrangedSubview(collectName!) + stackView.addArrangedSubview(collectCVV!) + stackView.addArrangedSubview(collectExpMonth!) + stackView.addArrangedSubview(collectExpYear!) + stackView.addArrangedSubview(collectSSN!) + stackView.addArrangedSubview(collectPhoneNumber!) + stackView.addArrangedSubview(collectLicenseNumber!) + stackView.addArrangedSubview(collectButton) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.backgroundColor = .white + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 20 + ).isActive = true + scrollView.leftAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leftAnchor, + constant: 10 + ).isActive = true + scrollView.rightAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.rightAnchor, + constant: -10 + ).isActive = true + scrollView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor + ).isActive = true + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + @objc func revealForm() { + self.revealContainer?.reveal(callback: ExampleAPICallback()) + } + @objc func submitForm() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + } + internal func updateSuccess(_ response: SuccessResponse) { + print(response) + updateRevealInputs(tokens: response.records[0].fields) + print("Successfully got response:", response) + } + internal func updateFailure(error: Any) { + print("Failed Operation", error) + } + internal func updateRevealInputs(tokens: Fields) { + let revealBaseStyle = SkyflowFlowVault.Style( + borderColor: UIColor.black, + cornerRadius: 20, + padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), + borderWidth: 2, + textAlignment: .left, + textColor: UIColor.blue + ) + let revealStyles = SkyflowFlowVault.Styles(base: revealBaseStyle) + DispatchQueue.main.async { + if self.revealed { + self.removeRevealElements() + } else { + self.revealed = true + } + let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( + token: tokens.card_number, + inputStyles: revealStyles, + label: "Card Number" + ) + self.revealCardNumber = self.revealContainer?.create( + input: revealCardNumberInput, + options: SkyflowFlowVault.RevealElementOptions(format: "XXXX-XXXX-XXXX-XXXX-XXX", translation: ["X": "[0-9]"]) + ) + let revealCVVtInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cvv, + inputStyles: revealStyles, + label: "CVV" + ) + self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) + let revealNameInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cardholder_name, + inputStyles: revealStyles, + label: "Card Holder Name" + ) + self.revealName = self.revealContainer?.create(input: revealNameInput) + let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( + token: tokens.expiry_month, + inputStyles: revealStyles, + label: "Expiration Month" + ) + self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) + let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( + token: tokens.expiry_year, + inputStyles: revealStyles, + label: "Expiration Year" + ) + self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) + let revealSSNInput = SkyflowFlowVault.RevealElementInput( + token: tokens.ssn, + inputStyles: revealStyles, + label: "SSN" + ) + self.revealSSN = self.revealContainer?.create(input: revealSSNInput, options: SkyflowFlowVault.RevealElementOptions(format: "XXX XX XXXX", translation: ["X": "[0-9]"])) + let revealPhoneNumberInput = SkyflowFlowVault.RevealElementInput( + token: tokens.phone_number, + inputStyles: revealStyles, + label: "Phone Number" + ) + self.revealPhoneNumber = self.revealContainer?.create(input: revealPhoneNumberInput, options: SkyflowFlowVault.RevealElementOptions(format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) + let revealLicenseNumberInput = SkyflowFlowVault.RevealElementInput( + token: tokens.phone_number, + inputStyles: revealStyles, + label: "License Number" + ) + self.revealLicenseNumber = self.revealContainer?.create(input: revealLicenseNumberInput, options: SkyflowFlowVault.RevealElementOptions(format: "X YYY YYY YY YY", translation: ["X": "[A-Z]","Y": "[0-9]"])) + self.addRevealElements() + } + } + + internal func removeRevealElements() { + self.stackView.removeArrangedSubview(self.revealCardNumber!) + self.stackView.removeArrangedSubview(self.revealName!) + self.stackView.removeArrangedSubview(self.revealCVV!) + self.stackView.removeArrangedSubview(self.revealExpirationMonth!) + self.stackView.removeArrangedSubview(self.revealExpirationYear!) + self.stackView.removeArrangedSubview(self.revealSSN!) + self.stackView.removeArrangedSubview(self.revealPhoneNumber!) + self.stackView.removeArrangedSubview(self.revealLicenseNumber!) + self.stackView.removeArrangedSubview(self.revealButton) + self.revealCardNumber?.removeFromSuperview() + self.revealName?.removeFromSuperview() + self.revealCVV?.removeFromSuperview() + self.revealExpirationMonth?.removeFromSuperview() + self.revealExpirationYear?.removeFromSuperview() + self.revealSSN?.removeFromSuperview() + self.revealPhoneNumber?.removeFromSuperview() + self.revealLicenseNumber?.removeFromSuperview() + self.revealButton.removeFromSuperview() + self.revealContainer = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + } + + internal func addRevealElements() { + DispatchQueue.main.async { + self.stackView.addArrangedSubview(self.revealCardNumber!) + self.stackView.addArrangedSubview(self.revealName!) + self.stackView.addArrangedSubview(self.revealCVV!) + self.stackView.addArrangedSubview(self.revealExpirationMonth!) + self.stackView.addArrangedSubview(self.revealExpirationYear!) + self.stackView.addArrangedSubview(self.revealSSN!) + self.stackView.addArrangedSubview(self.revealPhoneNumber!) + self.stackView.addArrangedSubview(self.revealLicenseNumber!) + self.stackView.addArrangedSubview(self.revealButton) + } + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift new file mode 100644 index 00000000..442b8c97 --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormattingTests/InputFormattingTests.swift @@ -0,0 +1,36 @@ +// +// InputFormattingTests.swift +// InputFormattingTests +// +// Created by Bharti Sagar on 26/04/23. +// + +import XCTest +@testable import InputFormatting + +final class InputFormattingTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift new file mode 100644 index 00000000..2741efeb --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITests.swift @@ -0,0 +1,41 @@ +// +// InputFormattingUITests.swift +// InputFormattingUITests +// +// Created by Bharti Sagar on 26/04/23. +// + +import XCTest + +final class InputFormattingUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift new file mode 100644 index 00000000..b9b6b48a --- /dev/null +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormattingUITests/InputFormattingUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// InputFormattingUITestsLaunchTests.swift +// InputFormattingUITests +// +// Created by Bharti Sagar on 26/04/23. +// + +import XCTest + +final class InputFormattingUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Samples/Skyflow/InputFormatting/Podfile b/SkyflowFlowVault/Samples/InputFormatting/Podfile similarity index 89% rename from Samples/Skyflow/InputFormatting/Podfile rename to SkyflowFlowVault/Samples/InputFormatting/Podfile index 354055ad..f1db8e2f 100644 --- a/Samples/Skyflow/InputFormatting/Podfile +++ b/SkyflowFlowVault/Samples/InputFormatting/Podfile @@ -3,8 +3,9 @@ target 'InputFormatting' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! + pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.25.0" + pod 'SkyflowFlowVault' # Pods for InputFormatting diff --git a/SkyflowFlowVault/Samples/Package.swift b/SkyflowFlowVault/Samples/Package.swift new file mode 100644 index 00000000..8b0ce501 --- /dev/null +++ b/SkyflowFlowVault/Samples/Package.swift @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription diff --git a/SkyflowFlowVault/Samples/README.md b/SkyflowFlowVault/Samples/README.md new file mode 100644 index 00000000..0abd6e32 --- /dev/null +++ b/SkyflowFlowVault/Samples/README.md @@ -0,0 +1,30 @@ +# Samples + +Sample apps for the SkyflowFlowVault SDK (`pod 'SkyflowFlowVault'` / `import SkyflowFlowVault`): + +| Sample | Description | +|--------|-------------| +| [CollectAndRevealSample](CollectAndRevealSample/) | Collect card data with secure elements and reveal tokens back to values (includes card brand choice) | +| [ComposableElements](ComposableElements/) | Lay out multiple secure elements together in shared rows using a composable container | +| [InputFormatting](InputFormatting/) | Apply `format`/`translation` patterns to control how element input is displayed | +| [UpdateDataUsingElements](UpdateDataUsingElements/) | Update existing vault records through collect elements using a skyflow ID | +| [UpsertFeature](UpsertFeature/) | Insert-or-update records with upsert options on a unique column | +| [Validations](Validations/) | Built-in and custom validation rules (regex, length, element match) on collect elements | + +`Note`: `GetSample` exists only in the legacy [Skyflow SDK samples](../../Skyflow/Samples/) — the `get()`/`getById()` operations are not part of the FlowVault SDK. + +## Running a sample + +Please follow these below steps to run samples + +- Navigate to the desired sample +- In CMD, run the command, pod install +- Open the .xcworkspace file using xcode +- Click on Build and run + +`Note`: +In every sample, in SkyflowFlowVault.Configuration(), replace with the following fields: +1. Replace the placeholder "" with the correct vaultId you want to connect +2. Replace the placeholder "" with the correct vaultURL +3. Implement the bearer token endpoint using server side auth SDK and service account file. + Replace the placeholder "" with the bearer token endpoint which gives the bearerToken, implemented at your backend. \ No newline at end of file diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/Podfile b/SkyflowFlowVault/Samples/UpdateDataUsingElements/Podfile new file mode 100644 index 00000000..095a4060 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/Podfile @@ -0,0 +1,26 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'UpdateDataUsingElements' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for UpdateDataUsingElements + pod 'AEXML', '4.6.1' + pod 'SkyflowFlowVault' + + target 'UpdateDataUsingElementsTests' do + inherit! :search_paths + # Pods for testing + pod 'AEXML', '4.6.1' + pod 'SkyflowFlowVault' + end + + target 'UpdateDataUsingElementsUITests' do + # Pods for testing + pod 'AEXML', '4.6.1' + pod 'SkyflowFlowVault' + + end + +end diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ae700fd2 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcodeproj/project.pbxproj @@ -0,0 +1,756 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 84209722024122879D341F43 /* Pods_UpdateDataUsingElementsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */; }; + A9BAC9FE1923AA2AF6EBDD4A /* Pods_UpdateDataUsingElements.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */; }; + F9F0ED650060535674C02713 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 8EE9B1D32F5D47F600E676C7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8EE9B1B42F5D47EF00E676C7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8EE9B1BB2F5D47EF00E676C7; + remoteInfo = UpdateDataUsingElements; + }; + 8EE9B1DD2F5D47F600E676C7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8EE9B1B42F5D47EF00E676C7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8EE9B1BB2F5D47EF00E676C7; + remoteInfo = UpdateDataUsingElements; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElementsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElementsTests.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests.release.xcconfig"; sourceTree = ""; }; + 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig"; sourceTree = ""; }; + 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements.release.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements.release.xcconfig"; sourceTree = ""; }; + 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElementsTests.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests.debug.xcconfig"; sourceTree = ""; }; + 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UpdateDataUsingElements.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UpdateDataUsingElementsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UpdateDataUsingElementsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig"; sourceTree = ""; }; + DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UpdateDataUsingElements.debug.xcconfig"; path = "Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements.debug.xcconfig"; sourceTree = ""; }; + F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UpdateDataUsingElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 8EE9B1E42F5D47F600E676C7 /* Exceptions for "UpdateDataUsingElements" folder in "UpdateDataUsingElements" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 8EE9B1E42F5D47F600E676C7 /* Exceptions for "UpdateDataUsingElements" folder in "UpdateDataUsingElements" target */, + ); + path = UpdateDataUsingElements; + sourceTree = ""; + }; + 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = UpdateDataUsingElementsTests; + sourceTree = ""; + }; + 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = UpdateDataUsingElementsUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8EE9B1B92F5D47EF00E676C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A9BAC9FE1923AA2AF6EBDD4A /* Pods_UpdateDataUsingElements.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1CF2F5D47F600E676C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 84209722024122879D341F43 /* Pods_UpdateDataUsingElementsTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1D92F5D47F600E676C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F9F0ED650060535674C02713 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27820800536265CC8997BEDA /* Pods */ = { + isa = PBXGroup; + children = ( + DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */, + 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */, + D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */, + 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */, + 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */, + 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 46291EA2BCEB4448675D0DA8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F9A9A41A0A4EEE3FD38922C5 /* Pods_UpdateDataUsingElements.framework */, + A128FC9CED2A962A852D5181 /* Pods_UpdateDataUsingElements_UpdateDataUsingElementsUITests.framework */, + 0BB1BBDB3F2018A417C4B8F5 /* Pods_UpdateDataUsingElementsTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8EE9B1B32F5D47EF00E676C7 = { + isa = PBXGroup; + children = ( + 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */, + 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */, + 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, + 8EE9B1BD2F5D47EF00E676C7 /* Products */, + 27820800536265CC8997BEDA /* Pods */, + 46291EA2BCEB4448675D0DA8 /* Frameworks */, + ); + sourceTree = ""; + }; + 8EE9B1BD2F5D47EF00E676C7 /* Products */ = { + isa = PBXGroup; + children = ( + 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */, + 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */, + 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8EE9B1E52F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElements" */; + buildPhases = ( + 0F8A093288F474159BEDE644 /* [CP] Check Pods Manifest.lock */, + 8EE9B1B82F5D47EF00E676C7 /* Sources */, + 8EE9B1B92F5D47EF00E676C7 /* Frameworks */, + 8EE9B1BA2F5D47EF00E676C7 /* Resources */, + E938BD775733C7F94C478863 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 8EE9B1BE2F5D47EF00E676C7 /* UpdateDataUsingElements */, + ); + name = UpdateDataUsingElements; + productName = UpdateDataUsingElements; + productReference = 8EE9B1BC2F5D47EF00E676C7 /* UpdateDataUsingElements.app */; + productType = "com.apple.product-type.application"; + }; + 8EE9B1D12F5D47F600E676C7 /* UpdateDataUsingElementsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8EE9B1EA2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsTests" */; + buildPhases = ( + 8B6ABD965E2972F8AC0F597B /* [CP] Check Pods Manifest.lock */, + 8EE9B1CE2F5D47F600E676C7 /* Sources */, + 8EE9B1CF2F5D47F600E676C7 /* Frameworks */, + 8EE9B1D02F5D47F600E676C7 /* Resources */, + 87992577B2A3D0F9B4A74C66 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 8EE9B1D42F5D47F600E676C7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8EE9B1D52F5D47F600E676C7 /* UpdateDataUsingElementsTests */, + ); + name = UpdateDataUsingElementsTests; + productName = UpdateDataUsingElementsTests; + productReference = 8EE9B1D22F5D47F600E676C7 /* UpdateDataUsingElementsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 8EE9B1DB2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8EE9B1ED2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsUITests" */; + buildPhases = ( + E3F917F11A21B04E3AE532A9 /* [CP] Check Pods Manifest.lock */, + 8EE9B1D82F5D47F600E676C7 /* Sources */, + 8EE9B1D92F5D47F600E676C7 /* Frameworks */, + 8EE9B1DA2F5D47F600E676C7 /* Resources */, + 7ED4115DECEAE1F715BC3D6F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 8EE9B1DE2F5D47F600E676C7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8EE9B1DF2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, + ); + name = UpdateDataUsingElementsUITests; + productName = UpdateDataUsingElementsUITests; + productReference = 8EE9B1DC2F5D47F600E676C7 /* UpdateDataUsingElementsUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8EE9B1B42F5D47EF00E676C7 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 8EE9B1BB2F5D47EF00E676C7 = { + CreatedOnToolsVersion = 16.4; + }; + 8EE9B1D12F5D47F600E676C7 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 8EE9B1BB2F5D47EF00E676C7; + }; + 8EE9B1DB2F5D47F600E676C7 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 8EE9B1BB2F5D47EF00E676C7; + }; + }; + }; + buildConfigurationList = 8EE9B1B72F5D47EF00E676C7 /* Build configuration list for PBXProject "UpdateDataUsingElements" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8EE9B1B32F5D47EF00E676C7; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 8EE9B1F42F5D489500E676C7 /* XCRemoteSwiftPackageReference "skyflow-iOS" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 8EE9B1BD2F5D47EF00E676C7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */, + 8EE9B1D12F5D47F600E676C7 /* UpdateDataUsingElementsTests */, + 8EE9B1DB2F5D47F600E676C7 /* UpdateDataUsingElementsUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8EE9B1BA2F5D47EF00E676C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1D02F5D47F600E676C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1DA2F5D47F600E676C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0F8A093288F474159BEDE644 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElements-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7ED4115DECEAE1F715BC3D6F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 87992577B2A3D0F9B4A74C66 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElementsTests/Pods-UpdateDataUsingElementsTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8B6ABD965E2972F8AC0F597B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElementsTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E3F917F11A21B04E3AE532A9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E938BD775733C7F94C478863 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UpdateDataUsingElements/Pods-UpdateDataUsingElements-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8EE9B1B82F5D47EF00E676C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1CE2F5D47F600E676C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8EE9B1D82F5D47F600E676C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 8EE9B1D42F5D47F600E676C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; + targetProxy = 8EE9B1D32F5D47F600E676C7 /* PBXContainerItemProxy */; + }; + 8EE9B1DE2F5D47F600E676C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8EE9B1BB2F5D47EF00E676C7 /* UpdateDataUsingElements */; + targetProxy = 8EE9B1DD2F5D47F600E676C7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 8EE9B1E62F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DDEEAC96F7413BA8DED5CC43 /* Pods-UpdateDataUsingElements.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UpdateDataUsingElements/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElements; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8EE9B1E72F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 55AA84197F63011C6D16CA15 /* Pods-UpdateDataUsingElements.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UpdateDataUsingElements/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElements; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 8EE9B1E82F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8EE9B1E92F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8EE9B1EB2F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8C4FE748FB9BE72A6F211DA7 /* Pods-UpdateDataUsingElementsTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UpdateDataUsingElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UpdateDataUsingElements"; + }; + name = Debug; + }; + 8EE9B1EC2F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 19501CBC771B67CB4A422639 /* Pods-UpdateDataUsingElementsTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UpdateDataUsingElements.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UpdateDataUsingElements"; + }; + name = Release; + }; + 8EE9B1EE2F5D47F600E676C7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D9E9FF05FEC4438776BD37D1 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UpdateDataUsingElements; + }; + name = Debug; + }; + 8EE9B1EF2F5D47F600E676C7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 31FA5C255B7E2C37276B38B3 /* Pods-UpdateDataUsingElements-UpdateDataUsingElementsUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Skyflow.UpdateDataUsingElementsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UpdateDataUsingElements; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8EE9B1B72F5D47EF00E676C7 /* Build configuration list for PBXProject "UpdateDataUsingElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1E82F5D47F600E676C7 /* Debug */, + 8EE9B1E92F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8EE9B1E52F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1E62F5D47F600E676C7 /* Debug */, + 8EE9B1E72F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8EE9B1EA2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1EB2F5D47F600E676C7 /* Debug */, + 8EE9B1EC2F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8EE9B1ED2F5D47F600E676C7 /* Build configuration list for PBXNativeTarget "UpdateDataUsingElementsUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8EE9B1EE2F5D47F600E676C7 /* Debug */, + 8EE9B1EF2F5D47F600E676C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 8EE9B1F42F5D489500E676C7 /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.24.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + }; + rootObject = 8EE9B1B42F5D47EF00E676C7 /* Project object */; +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..28e92450 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift new file mode 100644 index 00000000..aa40b1e6 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// UpdateDataUsingElements +// +// Created by Bharti Sagar on 08/03/26. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift new file mode 100644 index 00000000..bd93892c --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleAPICallback: SkyflowFlowVault.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: ((Any) -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (Any) -> Void, updateFailure: @escaping (Any) -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if updateFailure != nil { + print("failure:", error) + return + } + print("onfailure", error) + } +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift new file mode 100644 index 00000000..a170d9fe --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift new file mode 100644 index 00000000..4b948ce6 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields? + let table: String + let skyflow_id: String? +} + +struct Fields: Codable { + let cardholder_name: String? + let card_number: String? + let expiry_month: String? + let expiry_year: String? + let cvv: String? + let skyflow_id: String? +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift new file mode 100644 index 00000000..6f4093b7 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// UpdateDataUsingElements +// +// Created by Bharti Sagar on 08/03/26. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift new file mode 100644 index 00000000..ebc6c24f --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import SkyflowFlowVault + +class ViewController: UIViewController { + var retryCount = 0 + private var skyflow: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var b: UIButton? + + private var stackView: UIStackView! + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + let tokenProvider = ExampleTokenProvider() + + let config = SkyflowFlowVault.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider, + options: SkyflowFlowVault.Options( + logLevel: SkyflowFlowVault.LogLevel.DEBUG + ) + ) + + self.skyflow = SkyflowFlowVault.initialize(config) + + if self.skyflow != nil { + let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COMPOSABLE, options: ContainerOptions(layout: [1,2,2], styles: Styles(base: Style(borderColor: UIColor.gray) ), errorTextStyles: Styles(base: Style(textColor: UIColor.red) ))) + self.container = container + self.stackView = UIStackView() + + let baseStyle = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) + + let completedStyle = SkyflowFlowVault.Style(textColor: UIColor.green) + + let invalidStyle = SkyflowFlowVault.Style(textColor: UIColor.red) + + let styles = SkyflowFlowVault.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "card_number", + inputStyles: styles, + label: "Card Number", + placeholder: "4111-1111-1111-1111", + type: SkyflowFlowVault.ElementType.CARD_NUMBER, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + + ) + let collectNameInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "cardholder_name", + inputStyles: styles, + label: "Card Holder Name", + placeholder: "John Doe", + type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + ) + let collectCVVInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "cvv", + inputStyles: styles, + label: "CVV", + placeholder: "***", + type: .CVV, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + ) + let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "expiry_month", + inputStyles: styles, + label: "Expiration Month", + placeholder: "MM", + type: .EXPIRATION_MONTH, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + + ) + let collectExpYearInput = SkyflowFlowVault.CollectElementInput( + table: "credit_cards", + column: "expiry_year", + inputStyles: styles, + label: "Expiration Year", + placeholder: "YYYY", + type: .EXPIRATION_YEAR, + skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + + ) + let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) + let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) + let collectName = container?.create(input: collectNameInput, options: requiredOption) + let collectCVV = container?.create(input: collectCVVInput, options: requiredOption) + let collectExpMonth = container?.create(input: collectExpMonthInput, options: requiredOption) + let collectExpYear = container?.create(input: collectExpYearInput, options: requiredOption) + let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + collectButton.backgroundColor = .blue + collectButton.setTitle("Submit", for: .normal) + collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + + do { + let composableView = try container?.getComposableView() + stackView.addArrangedSubview(composableView) + } catch { + print(error) + } + + stackView.addArrangedSubview(collectButton) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.backgroundColor = .white + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 20 + ).isActive = true + scrollView.leftAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leftAnchor, + constant: 10 + ).isActive = true + scrollView.rightAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.rightAnchor, + constant: -10 + ).isActive = true + scrollView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor + ).isActive = true + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + + @objc func submitForm() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + } + internal func updateSuccess(_ response: SuccessResponse) { + print(response) + retryCount = 0 + print("Successfully got response:", response) + } + internal func updateFailure(error: Any) { + if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + retryCount += 1 + submitForm() + } + print("Failed Operation", error) + } + +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift new file mode 100644 index 00000000..eada6780 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsTests/UpdateDataUsingElementsTests.swift @@ -0,0 +1,36 @@ +// +// UpdateDataUsingElementsTests.swift +// UpdateDataUsingElementsTests +// +// Created by Bharti Sagar on 08/03/26. +// + +import XCTest +@testable import UpdateDataUsingElements + +final class UpdateDataUsingElementsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift new file mode 100644 index 00000000..a921baab --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITests.swift @@ -0,0 +1,41 @@ +// +// UpdateDataUsingElementsUITests.swift +// UpdateDataUsingElementsUITests +// +// Created by Bharti Sagar on 08/03/26. +// + +import XCTest + +final class UpdateDataUsingElementsUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift new file mode 100644 index 00000000..81b9cb73 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElementsUITests/UpdateDataUsingElementsUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// UpdateDataUsingElementsUITestsLaunchTests.swift +// UpdateDataUsingElementsUITests +// +// Created by Bharti Sagar on 08/03/26. +// + +import XCTest + +final class UpdateDataUsingElementsUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/Podfile b/SkyflowFlowVault/Samples/UpsertFeature/Podfile new file mode 100644 index 00000000..059444c4 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/Podfile @@ -0,0 +1,4 @@ +source 'https://github.com/CocoaPods/Specs.git' +target 'UpsertFeature' do + pod 'SkyflowFlowVault' +end diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj new file mode 100644 index 00000000..b06950f8 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 9818DE9829239C17004C72AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DE9729239C17004C72AE /* AppDelegate.swift */; }; + 9818DE9A29239C17004C72AE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DE9929239C17004C72AE /* SceneDelegate.swift */; }; + 9818DE9C29239C17004C72AE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DE9B29239C17004C72AE /* ViewController.swift */; }; + 9818DE9F29239C17004C72AE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9818DE9D29239C17004C72AE /* Main.storyboard */; }; + 9818DEA129239C1C004C72AE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9818DEA029239C1C004C72AE /* Assets.xcassets */; }; + 9818DEA429239C1C004C72AE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9818DEA229239C1C004C72AE /* LaunchScreen.storyboard */; }; + 9818DEAC29239C87004C72AE /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEAB29239C87004C72AE /* ResponseStructs.swift */; }; + 9818DEB029239CD0004C72AE /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */; }; + 9818DEB229239CD9004C72AE /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */; }; + 9818DEB82923B61D004C72AE /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 9818DEB72923B61D004C72AE /* Skyflow */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 9818DE9429239C17004C72AE /* UpsertFeature.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UpsertFeature.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9818DE9729239C17004C72AE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9818DE9929239C17004C72AE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 9818DE9B29239C17004C72AE /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 9818DE9E29239C17004C72AE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 9818DEA029239C1C004C72AE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9818DEA329239C1C004C72AE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 9818DEA529239C1C004C72AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9818DEAB29239C87004C72AE /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; + 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleTokenProvider.swift; path = ../../Upsert/Upsert/ExampleTokenProvider.swift; sourceTree = ""; }; + 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleAPICallback.swift; path = ../../Upsert/Upsert/ExampleAPICallback.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9818DE9129239C17004C72AE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9818DEB82923B61D004C72AE /* Skyflow in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9818DE8B29239C17004C72AE = { + isa = PBXGroup; + children = ( + 9818DE9629239C17004C72AE /* UpsertFeature */, + 9818DE9529239C17004C72AE /* Products */, + ); + sourceTree = ""; + }; + 9818DE9529239C17004C72AE /* Products */ = { + isa = PBXGroup; + children = ( + 9818DE9429239C17004C72AE /* UpsertFeature.app */, + ); + name = Products; + sourceTree = ""; + }; + 9818DE9629239C17004C72AE /* UpsertFeature */ = { + isa = PBXGroup; + children = ( + 9818DE9729239C17004C72AE /* AppDelegate.swift */, + 9818DE9929239C17004C72AE /* SceneDelegate.swift */, + 9818DE9B29239C17004C72AE /* ViewController.swift */, + 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */, + 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */, + 9818DEAB29239C87004C72AE /* ResponseStructs.swift */, + 9818DE9D29239C17004C72AE /* Main.storyboard */, + 9818DEA029239C1C004C72AE /* Assets.xcassets */, + 9818DEA229239C1C004C72AE /* LaunchScreen.storyboard */, + 9818DEA529239C1C004C72AE /* Info.plist */, + ); + path = UpsertFeature; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 9818DE9329239C17004C72AE /* UpsertFeature */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9818DEA829239C1C004C72AE /* Build configuration list for PBXNativeTarget "UpsertFeature" */; + buildPhases = ( + 9818DE9029239C17004C72AE /* Sources */, + 9818DE9129239C17004C72AE /* Frameworks */, + 9818DE9229239C17004C72AE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UpsertFeature; + packageProductDependencies = ( + 9818DEB72923B61D004C72AE /* Skyflow */, + ); + productName = UpsertFeature; + productReference = 9818DE9429239C17004C72AE /* UpsertFeature.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9818DE8C29239C17004C72AE /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + TargetAttributes = { + 9818DE9329239C17004C72AE = { + CreatedOnToolsVersion = 12.5.1; + }; + }; + }; + buildConfigurationList = 9818DE8F29239C17004C72AE /* Build configuration list for PBXProject "UpsertFeature" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9818DE8B29239C17004C72AE; + packageReferences = ( + 9818DEB62923B61D004C72AE /* XCRemoteSwiftPackageReference "skyflow-iOS" */, + ); + productRefGroup = 9818DE9529239C17004C72AE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 9818DE9329239C17004C72AE /* UpsertFeature */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9818DE9229239C17004C72AE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9818DEA429239C1C004C72AE /* LaunchScreen.storyboard in Resources */, + 9818DEA129239C1C004C72AE /* Assets.xcassets in Resources */, + 9818DE9F29239C17004C72AE /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 9818DE9029239C17004C72AE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9818DE9C29239C17004C72AE /* ViewController.swift in Sources */, + 9818DEB029239CD0004C72AE /* ExampleTokenProvider.swift in Sources */, + 9818DE9829239C17004C72AE /* AppDelegate.swift in Sources */, + 9818DEB229239CD9004C72AE /* ExampleAPICallback.swift in Sources */, + 9818DE9A29239C17004C72AE /* SceneDelegate.swift in Sources */, + 9818DEAC29239C87004C72AE /* ResponseStructs.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 9818DE9D29239C17004C72AE /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9818DE9E29239C17004C72AE /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 9818DEA229239C1C004C72AE /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9818DEA329239C1C004C72AE /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 9818DEA629239C1C004C72AE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 9818DEA729239C1C004C72AE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 9818DEA929239C1C004C72AE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = UpsertFeature/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = abc.UpsertFeature; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9818DEAA29239C1C004C72AE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = UpsertFeature/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = abc.UpsertFeature; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9818DE8F29239C17004C72AE /* Build configuration list for PBXProject "UpsertFeature" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9818DEA629239C1C004C72AE /* Debug */, + 9818DEA729239C1C004C72AE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9818DEA829239C1C004C72AE /* Build configuration list for PBXNativeTarget "UpsertFeature" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9818DEA929239C1C004C72AE /* Debug */, + 9818DEAA29239C1C004C72AE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 9818DEB62923B61D004C72AE /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; + requirement = { + kind = exactVersion; + version = 1.16.6; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 9818DEB72923B61D004C72AE /* Skyflow */ = { + isa = XCSwiftPackageProductDependency; + package = 9818DEB62923B61D004C72AE /* XCRemoteSwiftPackageReference "skyflow-iOS" */; + productName = Skyflow; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 9818DE8C29239C17004C72AE /* Project object */; +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..040de638 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "AEXML", + "repositoryURL": "https://github.com/tadija/AEXML.git", + "state": { + "branch": null, + "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version": "4.6.1" + } + }, + { + "package": "Skyflow", + "repositoryURL": "https://github.com/skyflowapi/skyflow-iOS.git", + "state": { + "branch": null, + "revision": "6598b75aac71dd6d573b2d15f59acedd5e9fa7dc", + "version": "1.16.6" + } + } + ] + }, + "version": 1 +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift new file mode 100644 index 00000000..ecaf4adf --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/AppDelegate.swift @@ -0,0 +1,30 @@ +// +// AppDelegate.swift +// UpsertFeature +// +// Created by Puneet Verma on 15/11/22. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Info.plist b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Info.plist new file mode 100644 index 00000000..5b531f7b --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift new file mode 100644 index 00000000..6ea1a8a6 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation + +struct SuccessResponse: Codable { + let records: [Records] +} + +struct Records: Codable { + let fields: Fields + let table: String +} + +struct Fields: Codable { + let cardnumber: String + let cvv: String + let skyflow_id: String +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift new file mode 100644 index 00000000..ce4a8ed2 --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/SceneDelegate.swift @@ -0,0 +1,47 @@ +// +// SceneDelegate.swift +// UpsertFeature +// +// Created by Puneet Verma on 15/11/22. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift new file mode 100644 index 00000000..060519aa --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import UIKit +import SkyflowFlowVault + +class ViewController: UIViewController { + private var skyflow: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var revealContainer: SkyflowFlowVault.Container? + private var b: UIButton? + private var stackView: UIStackView! + private var revealCardNumber: Label? + private var revealCvv: Label? + private var revealButton: UIButton! + private var revealed = false + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + let tokenProvider = ExampleTokenProvider() + let config = SkyflowFlowVault.Configuration(vaultID: "VAULT_ID", vaultURL: "VAULT_URL", tokenProvider: tokenProvider) + self.skyflow = SkyflowFlowVault.initialize(config) + + if self.skyflow != nil { + let container = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.COLLECT, options: nil) + self.container = container + self.stackView = UIStackView() + let baseStyle = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) ̰ + let completedStyle = SkyflowFlowVault.Style(borderColor: UIColor.green, textColor: UIColor.green) + let invalidStyle = SkyflowFlowVault.Style(borderColor: UIColor.red, textColor: UIColor.red) + let styles = SkyflowFlowVault.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + let baseStyleL = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left + ) + let focusStyleL = SkyflowFlowVault.Style(borderColor: .yellow) + let completedStyleL = SkyflowFlowVault.Style(borderColor: UIColor.green, textColor: UIColor.green) + let invalidStyleL = SkyflowFlowVault.Style(borderColor: UIColor.red, textColor: UIColor.red) + let labelStyles = SkyflowFlowVault.Styles( + base: baseStyleL, + complete: completedStyleL, + focus: focusStyleL, + invalid: invalidStyleL + ) + let baseStyleE = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .orange + ) + + let focusStyleE = SkyflowFlowVault.Style(borderColor: .yellow, textAlignment: .right, textColor: .yellow) + + let completedStyleE = SkyflowFlowVault.Style(borderColor: UIColor.green, textColor: UIColor.green) + + let invalidStyleE = SkyflowFlowVault.Style(borderColor: UIColor.red, textColor: UIColor.red) + let errorStyles = SkyflowFlowVault.Styles( + base: baseStyleE, + complete: completedStyleL, + focus: focusStyleE, + invalid: invalidStyleE + ) + // keep card number as unique column for testing upsert feature + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( + table: "persons", + column: "cardnumber", + inputStyles: styles, + labelStyles: labelStyles, + errorTextStyles: errorStyles, + label: "Card Number", + placeholder: "4111-1111-1111-1111", + type: SkyflowFlowVault.ElementType.CARD_NUMBER + ) + let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) + let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) + let collectCvvInput = SkyflowFlowVault.CollectElementInput( + table: "persons", + column: "cvv", + inputStyles: styles, + label: "Cvv", + placeholder: "123", + type: SkyflowFlowVault.ElementType.CVV + ) + let collectCvv = container?.create(input: collectCvvInput, options: requiredOption) + let collectButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + collectButton.backgroundColor = .blue + collectButton.setTitle("Submit", for: .normal) + collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + self.revealContainer = skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + self.revealButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + revealButton.backgroundColor = .blue + revealButton.setTitle("Reveal", for: .normal) + revealButton.addTarget(self, action: #selector(revealForm), for: .touchUpInside) + stackView.addArrangedSubview(collectCardNumber!) + stackView.addArrangedSubview(collectCvv!) + stackView.addArrangedSubview(collectButton) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + let scrollView = UIScrollView(frame: .zero) + scrollView.isScrollEnabled = true + scrollView.backgroundColor = .white + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 20 + ).isActive = true + scrollView.leftAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leftAnchor, + constant: 10 + ).isActive = true + scrollView.rightAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.rightAnchor, + constant: -10 + ).isActive = true + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + scrollView.addSubview(stackView) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true + } + } + + @objc func revealForm() { + self.revealContainer?.reveal(callback: ExampleAPICallback()) + } + + @objc func submitForm() { + let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) + let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] + container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + } + + internal func updateSuccess(_ response: SuccessResponse) { + updateRevealInputs(tokens: response.records[0].fields) + print("Successfully got response:", response) + } + + internal func updateFailure() { + print("Failed Operation") + } + + internal func updateRevealInputs(tokens: Fields) { + let revealBaseStyle = SkyflowFlowVault.Style( + borderColor: UIColor.black, + cornerRadius: 20, + padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), + borderWidth: 2, + textAlignment: .left, + textColor: UIColor.blue + ) + let revealStyles = SkyflowFlowVault.Styles(base: revealBaseStyle) + DispatchQueue.main.async { + if self.revealed { + self.removeRevealElements() + } else { + self.revealed = true + } + let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cardnumber, + inputStyles: revealStyles, + label: "Card Number", + redaction: .DEFAULT + ) + self.revealCardNumber = self.revealContainer?.create( + input: revealCardNumberInput, + options: SkyflowFlowVault.RevealElementOptions() + ) + let revealCvvInput = SkyflowFlowVault.RevealElementInput( + token: tokens.cvv, + inputStyles: revealStyles, + label: "Cvv", + redaction: .DEFAULT + ) + self.revealCvv = self.revealContainer?.create( + input: revealCvvInput, + options: SkyflowFlowVault.RevealElementOptions() + ) + self.addRevealElements() + } + } + + internal func removeRevealElements() { + self.stackView.removeArrangedSubview(self.revealCardNumber!) + self.stackView.removeArrangedSubview(self.revealCvv!) + self.stackView.removeArrangedSubview(self.revealButton) + self.revealCardNumber?.removeFromSuperview() + self.revealButton.removeFromSuperview() + self.revealContainer = self.skyflow?.container(type: SkyflowFlowVault.ContainerType.REVEAL, options: nil) + } + + internal func addRevealElements() { + DispatchQueue.main.async { + self.stackView.addArrangedSubview(self.revealCardNumber!) + self.stackView.addArrangedSubview(self.revealCvv!) + self.stackView.addArrangedSubview(self.revealButton) + } + } +} diff --git a/Samples/Skyflow/Validations/Podfile b/SkyflowFlowVault/Samples/Validations/Podfile similarity index 82% rename from Samples/Skyflow/Validations/Podfile rename to SkyflowFlowVault/Samples/Validations/Podfile index e8fecb94..1d1732a9 100644 --- a/Samples/Skyflow/Validations/Podfile +++ b/SkyflowFlowVault/Samples/Validations/Podfile @@ -5,7 +5,8 @@ source 'https://github.com/CocoaPods/Specs.git' target 'Validations' do #use_frameworks! + pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.25.0" + pod 'SkyflowFlowVault' end diff --git a/SkyflowFlowVault/Samples/Validations/README.md b/SkyflowFlowVault/Samples/Validations/README.md new file mode 100644 index 00000000..3ca50dbe --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/README.md @@ -0,0 +1 @@ +This sample illustrates how to apply custom validation rules on secure Skyflow Collect elements to restrict the type of input a user can provide. \ No newline at end of file diff --git a/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj new file mode 100644 index 00000000..afc0c387 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj @@ -0,0 +1,344 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + F1030006274E0A0D008F614B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030005274E0A0D008F614B /* AppDelegate.swift */; }; + F1030008274E0A0D008F614B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030007274E0A0D008F614B /* SceneDelegate.swift */; }; + F103000A274E0A0D008F614B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030009274E0A0D008F614B /* ViewController.swift */; }; + F103000D274E0A0D008F614B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F103000B274E0A0D008F614B /* Main.storyboard */; }; + F103000F274E0A12008F614B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F103000E274E0A12008F614B /* Assets.xcassets */; }; + F1030012274E0A12008F614B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1030010274E0A12008F614B /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + F1030002274E0A0D008F614B /* Validations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Validations.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F1030005274E0A0D008F614B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F1030007274E0A0D008F614B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + F1030009274E0A0D008F614B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + F103000C274E0A0D008F614B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + F103000E274E0A12008F614B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F1030011274E0A12008F614B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F1030013274E0A12008F614B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F103FFFF274E0A0D008F614B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F1030003274E0A0D008F614B /* Products */ = { + isa = PBXGroup; + children = ( + F1030002274E0A0D008F614B /* Validations.app */, + ); + name = Products; + sourceTree = ""; + }; + F1030004274E0A0D008F614B /* Validations */ = { + isa = PBXGroup; + children = ( + F1030005274E0A0D008F614B /* AppDelegate.swift */, + F1030007274E0A0D008F614B /* SceneDelegate.swift */, + F1030009274E0A0D008F614B /* ViewController.swift */, + F103000B274E0A0D008F614B /* Main.storyboard */, + F103000E274E0A12008F614B /* Assets.xcassets */, + F1030010274E0A12008F614B /* LaunchScreen.storyboard */, + F1030013274E0A12008F614B /* Info.plist */, + ); + path = Validations; + sourceTree = ""; + }; + F103FFF9274E0A0D008F614B = { + isa = PBXGroup; + children = ( + F1030004274E0A0D008F614B /* Validations */, + F1030003274E0A0D008F614B /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F1030001274E0A0D008F614B /* Validations */ = { + isa = PBXNativeTarget; + buildConfigurationList = F1030016274E0A12008F614B /* Build configuration list for PBXNativeTarget "Validations" */; + buildPhases = ( + F103FFFE274E0A0D008F614B /* Sources */, + F103FFFF274E0A0D008F614B /* Frameworks */, + F1030000274E0A0D008F614B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Validations; + productName = Validations; + productReference = F1030002274E0A0D008F614B /* Validations.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F103FFFA274E0A0D008F614B /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1200; + TargetAttributes = { + F1030001274E0A0D008F614B = { + CreatedOnToolsVersion = 12.0.1; + }; + }; + }; + buildConfigurationList = F103FFFD274E0A0D008F614B /* Build configuration list for PBXProject "Validations" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F103FFF9274E0A0D008F614B; + productRefGroup = F1030003274E0A0D008F614B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F1030001274E0A0D008F614B /* Validations */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F1030000274E0A0D008F614B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F1030012274E0A12008F614B /* LaunchScreen.storyboard in Resources */, + F103000F274E0A12008F614B /* Assets.xcassets in Resources */, + F103000D274E0A0D008F614B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F103FFFE274E0A0D008F614B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F103000A274E0A0D008F614B /* ViewController.swift in Sources */, + F1030006274E0A0D008F614B /* AppDelegate.swift in Sources */, + F1030008274E0A0D008F614B /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + F103000B274E0A0D008F614B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F103000C274E0A0D008F614B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + F1030010274E0A12008F614B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F1030011274E0A12008F614B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F1030014274E0A12008F614B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F1030015274E0A12008F614B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F1030017274E0A12008F614B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = Validations/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.Validations; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F1030018274E0A12008F614B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = Validations/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.Validations; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F1030016274E0A12008F614B /* Build configuration list for PBXNativeTarget "Validations" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F1030017274E0A12008F614B /* Debug */, + F1030018274E0A12008F614B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F103FFFD274E0A0D008F614B /* Build configuration list for PBXProject "Validations" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F1030014274E0A12008F614B /* Debug */, + F1030015274E0A12008F614B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F103FFFA274E0A0D008F614B /* Project object */; +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SkyflowFlowVault/Samples/Validations/Validations/AppDelegate.swift b/SkyflowFlowVault/Samples/Validations/Validations/AppDelegate.swift new file mode 100644 index 00000000..ac3bbcec --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/AppDelegate.swift @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// +// AppDelegate.swift +// Validations +// +// Created by Akhil Anil Mangala on 24/11/21. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json b/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json b/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/Contents.json b/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard b/SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/Main.storyboard b/SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift new file mode 100644 index 00000000..48f4c77a --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// +// ExampleAPICallback.swift +// Validations +// +// Created by Akhil Anil Mangala on 24/11/21. +// + +import Foundation +import SkyflowFlowVault + +public class ExampleAPICallback: SkyflowFlowVault.Callback { + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + print("success:", responseBody) + } + + public func onFailure(_ error: Any) { + print("failure:", error) + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/ExampleTokenProvider.swift b/SkyflowFlowVault/Samples/Validations/Validations/ExampleTokenProvider.swift new file mode 100644 index 00000000..93c50106 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/ExampleTokenProvider.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// +// ExampleTokenProvider.swift +// Validations +// +// Created by Akhil Anil Mangala on 24/11/21. +// + +import Foundation +import SkyflowFlowVault + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/Info.plist b/SkyflowFlowVault/Samples/Validations/Validations/Info.plist new file mode 100644 index 00000000..5b531f7b --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SkyflowFlowVault/Samples/Validations/Validations/SceneDelegate.swift b/SkyflowFlowVault/Samples/Validations/Validations/SceneDelegate.swift new file mode 100644 index 00000000..cd7bc458 --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/SceneDelegate.swift @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// +// SceneDelegate.swift +// Validations +// +// Created by Akhil Anil Mangala on 24/11/21. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift b/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift new file mode 100644 index 00000000..972262ff --- /dev/null +++ b/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +// Reset Password - A simple example that illustrates custom validations. +// The below code shows two input fields with custom validations, +// one to enter a password and the second to confirm the same password. + +import UIKit +import SkyflowFlowVault + +class ViewController: UIViewController { + private var skyflowClient: SkyflowFlowVault.Client? + private var container: SkyflowFlowVault.Container? + private var b: UIButton? + private var confirmPasswordElement: SkyflowFlowVault.TextField? + + private var stackView: UIStackView! + + override func loadView() { + let view = UIView() + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + let tokenProvider = ExampleTokenProvider() + let config = SkyflowFlowVault.Configuration( + vaultID: "", + vaultURL: "", + tokenProvider: tokenProvider + ) + self.skyflowClient = SkyflowFlowVault.initialize(config) + if self.skyflowClient != nil { + let container = self.skyflowClient?.container(type: SkyflowFlowVault.ContainerType.COLLECT, options: nil) + self.container = container + self.stackView = UIStackView() + let baseStyle = SkyflowFlowVault.Style( + cornerRadius: 2, + padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), + borderWidth: 1, + textAlignment: .left, + textColor: .blue + ) + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) + let completedStyle = SkyflowFlowVault.Style( + borderColor: UIColor.green, + textColor: UIColor.green + ) + let invalidStyle = SkyflowFlowVault.Style( + borderColor: UIColor.red, + textColor: UIColor.red + ) + let styles = SkyflowFlowVault.Styles( + base: baseStyle, + complete: completedStyle, + focus: focusStyle, + invalid: invalidStyle + ) + var myRuleset = ValidationSet() + // This rule enforces a strong password + let strongPasswordRule = RegexMatchRule( + regex: "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]*$", + error: "At least one letter and one number" + ) + // This rule allows input length between 8 and 16 characters + let lengthRule = LengthMatchRule( + minLength: 8, + maxLength: 16, + error: "Must be between 8 and 16 digits" + ) + // For the Password element + myRuleset.add(rule: strongPasswordRule) + myRuleset.add(rule: lengthRule) + let collectElementOptions = CollectElementOptions(required: true) + let passwordInput = CollectElementInput( + inputStyles: styles, + label: "password", + placeholder: "********", + type: .INPUT_FIELD, + validations: myRuleset + ) + let password = container?.create( + input: passwordInput, + options: collectElementOptions + ) + // For confirm password element - shows error when the passwords don't match + let elementValueMatchRule = ElementValueMatchRule( + element: password!, + error: "passwords don't match" + ) + let confirmPasswordInput = CollectElementInput( + inputStyles: styles, + label: "Confirm password", + placeholder: "********", + type: .INPUT_FIELD, + validations: ValidationSet(rules: [strongPasswordRule, lengthRule, elementValueMatchRule]) + ) + let confirmPassword = container?.create(input: confirmPasswordInput, options: collectElementOptions) + self.confirmPasswordElement = confirmPassword + // mount elements on screen - errors will be shown if any of the validaitons fail + stackView.addArrangedSubview(password!) + stackView.addArrangedSubview(confirmPassword!) + let resetButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) + resetButton.backgroundColor = .blue + resetButton.setTitle("Reset Password", for: .normal) + resetButton.addTarget(self, action: #selector(resetPassword), for: .touchUpInside) + stackView.addArrangedSubview(resetButton) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(stackView) + stackView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 50).isActive = true + stackView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10).isActive = true + stackView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10).isActive = true + } + } + + @objc func resetPassword() { + let url = "" + let requestHeaders = ["Content-Type": "application/json ", "Authorization": ""] + let requestBody: [String: Any] = [ + // Other fields... + "password": self.confirmPasswordElement! + ] + let responseBody: [String: Any] = [:] + let connectionConfig = ConnectionConfig( + connectionURL: url, + method: .POST, + requestBody: requestBody, + requestHeader: requestHeaders, + responseBody: responseBody + ) + self.skyflowClient?.invokeConnection(config: connectionConfig, callback: ExampleAPICallback()) + } +} diff --git a/Sources/SkyflowFlowVault/Exports.swift b/SkyflowFlowVault/Sources/Exports.swift similarity index 100% rename from Sources/SkyflowFlowVault/Exports.swift rename to SkyflowFlowVault/Sources/Exports.swift diff --git a/Sources/SkyflowFlowVault/Init.swift b/SkyflowFlowVault/Sources/Init.swift similarity index 100% rename from Sources/SkyflowFlowVault/Init.swift rename to SkyflowFlowVault/Sources/Init.swift diff --git a/Sources/SkyflowFlowVault/client/ClientOperations.swift b/SkyflowFlowVault/Sources/client/ClientOperations.swift similarity index 100% rename from Sources/SkyflowFlowVault/client/ClientOperations.swift rename to SkyflowFlowVault/Sources/client/ClientOperations.swift diff --git a/Sources/SkyflowFlowVault/collect/CollectCallback.swift b/SkyflowFlowVault/Sources/collect/CollectCallback.swift similarity index 100% rename from Sources/SkyflowFlowVault/collect/CollectCallback.swift rename to SkyflowFlowVault/Sources/collect/CollectCallback.swift diff --git a/Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift b/SkyflowFlowVault/Sources/collect/CollectContainerOperations.swift similarity index 100% rename from Sources/SkyflowFlowVault/collect/CollectContainerOperations.swift rename to SkyflowFlowVault/Sources/collect/CollectContainerOperations.swift diff --git a/Sources/SkyflowFlowVault/collect/CollectRequestBuilder.swift b/SkyflowFlowVault/Sources/collect/CollectRequestBuilder.swift similarity index 100% rename from Sources/SkyflowFlowVault/collect/CollectRequestBuilder.swift rename to SkyflowFlowVault/Sources/collect/CollectRequestBuilder.swift diff --git a/Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift b/SkyflowFlowVault/Sources/composable/ComposableContainerOperations.swift similarity index 100% rename from Sources/SkyflowFlowVault/composable/ComposableContainerOperations.swift rename to SkyflowFlowVault/Sources/composable/ComposableContainerOperations.swift diff --git a/Sources/SkyflowFlowVault/composable/ContainerOptions.swift b/SkyflowFlowVault/Sources/composable/ContainerOptions.swift similarity index 100% rename from Sources/SkyflowFlowVault/composable/ContainerOptions.swift rename to SkyflowFlowVault/Sources/composable/ContainerOptions.swift diff --git a/Sources/SkyflowFlowVault/models/AdditionalFields.swift b/SkyflowFlowVault/Sources/models/AdditionalFields.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/AdditionalFields.swift rename to SkyflowFlowVault/Sources/models/AdditionalFields.swift diff --git a/Sources/SkyflowFlowVault/models/CollectElementInput.swift b/SkyflowFlowVault/Sources/models/CollectElementInput.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/CollectElementInput.swift rename to SkyflowFlowVault/Sources/models/CollectElementInput.swift diff --git a/Sources/SkyflowFlowVault/models/CollectElementOptions.swift b/SkyflowFlowVault/Sources/models/CollectElementOptions.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/CollectElementOptions.swift rename to SkyflowFlowVault/Sources/models/CollectElementOptions.swift diff --git a/Sources/SkyflowFlowVault/models/CollectOptions.swift b/SkyflowFlowVault/Sources/models/CollectOptions.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/CollectOptions.swift rename to SkyflowFlowVault/Sources/models/CollectOptions.swift diff --git a/Sources/SkyflowFlowVault/models/CollectResponse.swift b/SkyflowFlowVault/Sources/models/CollectResponse.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/CollectResponse.swift rename to SkyflowFlowVault/Sources/models/CollectResponse.swift diff --git a/Sources/SkyflowFlowVault/models/Configuration.swift b/SkyflowFlowVault/Sources/models/Configuration.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/Configuration.swift rename to SkyflowFlowVault/Sources/models/Configuration.swift diff --git a/Sources/SkyflowFlowVault/models/InsertOptions.swift b/SkyflowFlowVault/Sources/models/InsertOptions.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/InsertOptions.swift rename to SkyflowFlowVault/Sources/models/InsertOptions.swift diff --git a/Sources/SkyflowFlowVault/models/RevealElementInput.swift b/SkyflowFlowVault/Sources/models/RevealElementInput.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/RevealElementInput.swift rename to SkyflowFlowVault/Sources/models/RevealElementInput.swift diff --git a/Sources/SkyflowFlowVault/models/RevealElementOptions.swift b/SkyflowFlowVault/Sources/models/RevealElementOptions.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/RevealElementOptions.swift rename to SkyflowFlowVault/Sources/models/RevealElementOptions.swift diff --git a/Sources/SkyflowFlowVault/models/RevealMetadata.swift b/SkyflowFlowVault/Sources/models/RevealMetadata.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/RevealMetadata.swift rename to SkyflowFlowVault/Sources/models/RevealMetadata.swift diff --git a/Sources/SkyflowFlowVault/models/RevealOptions.swift b/SkyflowFlowVault/Sources/models/RevealOptions.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/RevealOptions.swift rename to SkyflowFlowVault/Sources/models/RevealOptions.swift diff --git a/Sources/SkyflowFlowVault/models/RevealRequestRecord.swift b/SkyflowFlowVault/Sources/models/RevealRequestRecord.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/RevealRequestRecord.swift rename to SkyflowFlowVault/Sources/models/RevealRequestRecord.swift diff --git a/Sources/SkyflowFlowVault/models/RevealResponse.swift b/SkyflowFlowVault/Sources/models/RevealResponse.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/RevealResponse.swift rename to SkyflowFlowVault/Sources/models/RevealResponse.swift diff --git a/Sources/SkyflowFlowVault/models/Token.swift b/SkyflowFlowVault/Sources/models/Token.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/Token.swift rename to SkyflowFlowVault/Sources/models/Token.swift diff --git a/Sources/SkyflowFlowVault/models/TokenGroupRedaction.swift b/SkyflowFlowVault/Sources/models/TokenGroupRedaction.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/TokenGroupRedaction.swift rename to SkyflowFlowVault/Sources/models/TokenGroupRedaction.swift diff --git a/Sources/SkyflowFlowVault/models/UpsertOption.swift b/SkyflowFlowVault/Sources/models/UpsertOption.swift similarity index 100% rename from Sources/SkyflowFlowVault/models/UpsertOption.swift rename to SkyflowFlowVault/Sources/models/UpsertOption.swift diff --git a/Sources/SkyflowFlowVault/networking/APIClientFlowVault.swift b/SkyflowFlowVault/Sources/networking/APIClientFlowVault.swift similarity index 100% rename from Sources/SkyflowFlowVault/networking/APIClientFlowVault.swift rename to SkyflowFlowVault/Sources/networking/APIClientFlowVault.swift diff --git a/Sources/SkyflowFlowVault/networking/FlowVaultCollectAPICallback.swift b/SkyflowFlowVault/Sources/networking/FlowVaultCollectAPICallback.swift similarity index 100% rename from Sources/SkyflowFlowVault/networking/FlowVaultCollectAPICallback.swift rename to SkyflowFlowVault/Sources/networking/FlowVaultCollectAPICallback.swift diff --git a/Sources/SkyflowFlowVault/networking/FlowVaultRevealApiCallback.swift b/SkyflowFlowVault/Sources/networking/FlowVaultRevealApiCallback.swift similarity index 100% rename from Sources/SkyflowFlowVault/networking/FlowVaultRevealApiCallback.swift rename to SkyflowFlowVault/Sources/networking/FlowVaultRevealApiCallback.swift diff --git a/Sources/SkyflowFlowVault/reveal/RevealCallback.swift b/SkyflowFlowVault/Sources/reveal/RevealCallback.swift similarity index 100% rename from Sources/SkyflowFlowVault/reveal/RevealCallback.swift rename to SkyflowFlowVault/Sources/reveal/RevealCallback.swift diff --git a/Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift b/SkyflowFlowVault/Sources/reveal/RevealContainerOperations.swift similarity index 100% rename from Sources/SkyflowFlowVault/reveal/RevealContainerOperations.swift rename to SkyflowFlowVault/Sources/reveal/RevealContainerOperations.swift diff --git a/Sources/SkyflowFlowVault/reveal/RevealRequestBuilder.swift b/SkyflowFlowVault/Sources/reveal/RevealRequestBuilder.swift similarity index 100% rename from Sources/SkyflowFlowVault/reveal/RevealRequestBuilder.swift rename to SkyflowFlowVault/Sources/reveal/RevealRequestBuilder.swift diff --git a/Sources/SkyflowFlowVault/reveal/RevealValueCallback.swift b/SkyflowFlowVault/Sources/reveal/RevealValueCallback.swift similarity index 100% rename from Sources/SkyflowFlowVault/reveal/RevealValueCallback.swift rename to SkyflowFlowVault/Sources/reveal/RevealValueCallback.swift diff --git a/Sources/SkyflowFlowVault/utils/CVVTokenReplacer.swift b/SkyflowFlowVault/Sources/utils/CVVTokenReplacer.swift similarity index 100% rename from Sources/SkyflowFlowVault/utils/CVVTokenReplacer.swift rename to SkyflowFlowVault/Sources/utils/CVVTokenReplacer.swift diff --git a/Sources/SkyflowFlowVault/validations/RequestValidators.swift b/SkyflowFlowVault/Sources/validations/RequestValidators.swift similarity index 100% rename from Sources/SkyflowFlowVault/validations/RequestValidators.swift rename to SkyflowFlowVault/Sources/validations/RequestValidators.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-collectTests/DemoImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-collectTests/DemoImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-collectTests/DemoImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/XCTestManifests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-collectTests/XCTestManifests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-collectTests/XCTestManifests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-collectTests/XCTestManifests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-collectTests/skyflow_iOS_emptySkyflowIdValidationTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/DemoImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-composableTests/DemoImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-composableTests/DemoImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/CollectElementOptionsTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/CollectElementOptionsTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/CollectElementOptionsTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/DemoImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/DemoImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/DemoImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/InputFormattingTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/InputFormattingTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/TextFieldDelegateTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/TextFieldDelegateTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/XCTestManifests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/XCTestManifests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/XCTestManifests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/XCTestManifests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/skyflow_iOS_containerTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/DemoImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/DemoImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/DemoImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/GatewayImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/GatewayImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/GatewayImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealOptionsValidationTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-errorTests/XCTestManifests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-errorTests/XCTestManifests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-errorTests/XCTestManifests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-errorTests/XCTestManifests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-revealTests/DemoImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-revealTests/DemoImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-revealTests/DemoImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealElementOptionsTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealElementOptionsTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-revealTests/RevealElementOptionsTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-revealTests/RevealSkyflowLabelViewTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/XCTestManifests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-revealTests/XCTestManifests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-revealTests/XCTestManifests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-revealTests/XCTestManifests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/DemoImpl.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/DemoImpl.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/DemoImpl.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/MockURLProtocol.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/MockURLProtocol.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenLifecycleTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/TokenLifecycleTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenLifecycleTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/TokenLifecycleTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenParsingTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/TokenParsingTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/TokenParsingTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/TokenParsingTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_cvvMockTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_flowVaultSdkTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift diff --git a/Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift similarity index 100% rename from Tests/SkyflowFlowVault/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift rename to SkyflowFlowVault/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index e6542de6..63677624 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -11,7 +11,7 @@ version=$1 SEMVER=$version PODSPECS="Skyflow.podspec SkyflowFlowVault.podspec" -VERSION_FILE="./Sources/SkyflowCore/Version.swift" +VERSION_FILE="./SkyflowCore/Sources/Version.swift" if [ -z $2 ] then From 8a09befa0fc702a13ccb547062fed478a5203c43 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 17 Aug 2026 11:39:25 +0530 Subject: [PATCH 11/16] SK-3043 remove build folder of samples --- .../debug/index/db/v13/p5445--a626e6/data.mdb | Bin 67108864 -> 0 bytes .../debug/index/db/v13/p5445--a626e6/lock.mdb | Bin 8256 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/data.mdb delete mode 100644 SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/lock.mdb diff --git a/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/data.mdb b/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/data.mdb deleted file mode 100644 index aa89ad86a084cdab2661547bd094a70b0d905660..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67108864 zcmeF)JC4&(6adi2XoQFuO-Ta<3&4niYLpK(yGV#<#v-4Q?SRryP_qt}fe>N^EPw^j zu!I5s62;O)I0y-ij*C3MkMFzp*eZ*5lkcB@e!9~PRqb)vya=IMu{+uBUDro@ z{HA`hzP0B~%Z(EtK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB=D8DX{(S{$G6mzm2{WkDiV`4u1^a_3sY8 z^j{9Xj#`)M5+Fc;009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAn@M^Jo}x=Q8)?n<#aWRqs4e0XGL<=|A_BT^SVBo z82LE8i1WXTv05Z~G+9SQTkPK7pF%m?&ZCKOp3LIIV)Q~8BUzj;ivV3PLZxvUuF~b3 zWE!VYkw$6ysLXc%&u%RbLUHlcIKRvalxdR2ukvNOE@E_->6OtAy2W)>_d}CH()}^1 zW1NI+J)bOR(d%R}EnT`-vWc-N=J|fsa(37D;pPry(c2`yjPf|kqbj$~uRvW}ZyU4N z@qp*%IIOa?^V9MN#alvv009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7e?FLvla000000LcHf5fU6YaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2e3m20ssI206_k)jga8LfddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIDj2G5C8xG008oTZG;2|4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`zya*gfdBvi005BxYa=8$ zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M%C|4g>%I004meUmGF8fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB)i;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?utNs| z00000K>n|dkl?_90|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KfE_vz000000P=rrgaii;95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=0qoF$000000FeJ{BP2L*;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4q%541ONa40D$~o8zI4g0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KZ~!}WAOHXW0089w+6V~_ z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fdks1V*vmF0DwXM)`Qqv2pKS7z<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ f0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz`z&?wch3e diff --git a/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/lock.mdb b/SkyflowFlowVault/Samples/.build/index-build/arm64-apple-macosx/debug/index/db/v13/p5445--a626e6/lock.mdb deleted file mode 100644 index 36898a978b6aa290820fe0368a676bcf2403a638..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8256 zcmeIuJq|!X5P(rc?F?Fto9JjH&cF>gf;fT1J;X`0N-p3xyDNeS(SF_W?oYr`&xvNw4=lfXuW=?vVqvr4b4pFRj+20<5 zYk&2#Kl|686rcJZRwIP~0tg_000IagfB*srAb Date: Mon, 17 Aug 2026 12:10:46 +0530 Subject: [PATCH 12/16] SK-3043 add tests in root for both sdk --- Package.swift | 7 +- .../CoexistenceTests.swift | 155 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 Tests/skyflow-iOS-coexistenceTests/CoexistenceTests.swift diff --git a/Package.swift b/Package.swift index 4725c3d1..d0343733 100644 --- a/Package.swift +++ b/Package.swift @@ -75,6 +75,11 @@ let package = Package( path: "Skyflow/Tests/skyflow-iOS-getTests"), .testTarget(name: "skyflow-iOS-composableTests", dependencies: ["SkyflowFlowVault", "SkyflowCore"], - path: "SkyflowFlowVault/Tests/skyflow-iOS-composableTests") + path: "SkyflowFlowVault/Tests/skyflow-iOS-composableTests"), + // Dual-SDK coexistence tests: import both products side by side, + // public API only, the way an app installing both pods would. + .testTarget(name: "skyflow-iOS-coexistenceTests", + dependencies: ["Skyflow", "SkyflowFlowVault"], + path: "Tests/skyflow-iOS-coexistenceTests") ] ) diff --git a/Tests/skyflow-iOS-coexistenceTests/CoexistenceTests.swift b/Tests/skyflow-iOS-coexistenceTests/CoexistenceTests.swift new file mode 100644 index 00000000..f7e25579 --- /dev/null +++ b/Tests/skyflow-iOS-coexistenceTests/CoexistenceTests.swift @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025 Skyflow +*/ + +// Coexistence tests: both SDKs imported side by side, the way an app that +// installs both products would. No @testable — only the public API surface. +// +// What coexistence means here: +// - Shared core types (Client, Container, ContainerType, TextField, Label, +// Callback, TokenProvider, ...) are re-exported by BOTH SDKs but are the +// SAME declarations, so unqualified use stays unambiguous. +// - Per-SDK wrapper types (Configuration, CollectElementInput, options, ...) +// are DISTINCT declarations with the same names, so they are used with +// module qualification: Skyflow.Configuration vs SkyflowFlowVault.Configuration. +// - Same-named member overloads contributed by both SDKs to the shared Client +// (e.g. container(type:options:)) are disambiguated by their per-SDK +// parameter types. + +import XCTest +import Skyflow +import SkyflowFlowVault + +final class skyflow_iOS_coexistenceTests: XCTestCase { + // TokenProvider and Callback are shared core protocols — one conformance + // serves both SDKs. + class DemoTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + apiCallback.onSuccess("demo-token") + } + } + + class FailureExpectingCallback: Callback { + let expectation: XCTestExpectation + var receivedError: Any? + + init(expectation: XCTestExpectation) { + self.expectation = expectation + } + + func onSuccess(_ responseBody: Any) { + XCTFail("expected onFailure, got onSuccess: \(responseBody)") + } + + func onFailure(_ error: Any) { + receivedError = error + expectation.fulfill() + } + } + + private func makeLegacyClient() -> Client { + return Skyflow.initialize(Skyflow.Configuration( + vaultID: "legacyVaultID", + vaultURL: "https://legacy.vault.example.org", + tokenProvider: DemoTokenProvider())) + } + + private func makeFlowVaultClient() -> Client { + return SkyflowFlowVault.initialize(SkyflowFlowVault.Configuration( + vaultID: "flowVaultID", + vaultURL: "https://flow.vault.example.org", + tokenProvider: DemoTokenProvider())) + } + + // MARK: - Initialization + + func testBothClientsInitializeSideBySide() { + let legacyClient = makeLegacyClient() + let flowVaultClient = makeFlowVaultClient() + + XCTAssertNotNil(legacyClient) + XCTAssertNotNil(flowVaultClient) + // Both SDKs hand out the one shared core Client class. + XCTAssertTrue(type(of: legacyClient) == type(of: flowVaultClient)) + } + + // MARK: - Per-SDK wrapper types stay distinct + + func testPerSDKWrapperTypesAreDistinctDeclarations() { + XCTAssertNotEqual(String(reflecting: Skyflow.Configuration.self), + String(reflecting: SkyflowFlowVault.Configuration.self)) + XCTAssertNotEqual(String(reflecting: Skyflow.CollectElementInput.self), + String(reflecting: SkyflowFlowVault.CollectElementInput.self)) + XCTAssertNotEqual(String(reflecting: Skyflow.CollectOptions.self), + String(reflecting: SkyflowFlowVault.CollectOptions.self)) + XCTAssertNotEqual(String(reflecting: Skyflow.RevealOptions.self), + String(reflecting: SkyflowFlowVault.RevealOptions.self)) + } + + // MARK: - Containers and elements from each SDK + + func testCollectContainersAndElementsFromEachSDK() { + let legacyClient = makeLegacyClient() + let flowVaultClient = makeFlowVaultClient() + + // Both SDKs extend the shared Client with container(type:options:). + // A typed options value selects the overload; ContainerType and the + // returned Container are shared core types. + let legacyOptions: Skyflow.ContainerOptions? = nil + let legacyCollect = legacyClient.container(type: ContainerType.COLLECT, options: legacyOptions) + XCTAssertNotNil(legacyCollect) + + let flowVaultOptions: SkyflowFlowVault.ContainerOptions? = nil + let flowVaultCollect = flowVaultClient.container(type: ContainerType.COLLECT, options: flowVaultOptions) + XCTAssertNotNil(flowVaultCollect) + + // Element creation uses each SDK's own input wrapper; the produced + // TextField is the shared core element type. + let legacyElement = legacyCollect?.create(input: Skyflow.CollectElementInput( + table: "cards", column: "card_number", type: .CARD_NUMBER)) + XCTAssertNotNil(legacyElement) + + let flowVaultElement = flowVaultCollect?.create(input: SkyflowFlowVault.CollectElementInput( + tableName: "cards", column: "card_number", type: .CARD_NUMBER)) + XCTAssertNotNil(flowVaultElement) + + XCTAssertTrue(type(of: legacyElement!) == type(of: flowVaultElement!)) + } + + func testRevealContainersFromEachSDK() { + let legacyClient = makeLegacyClient() + let flowVaultClient = makeFlowVaultClient() + + let legacyOptions: Skyflow.ContainerOptions? = nil + let legacyReveal = legacyClient.container(type: ContainerType.REVEAL, options: legacyOptions) + XCTAssertNotNil(legacyReveal) + + let flowVaultOptions: SkyflowFlowVault.ContainerOptions? = nil + let flowVaultReveal = flowVaultClient.container(type: ContainerType.REVEAL, options: flowVaultOptions) + XCTAssertNotNil(flowVaultReveal) + + let legacyLabel = legacyReveal?.create(input: Skyflow.RevealElementInput( + token: "some-token", label: "card number")) + XCTAssertNotNil(legacyLabel) + + let flowVaultLabel = flowVaultReveal?.create(input: SkyflowFlowVault.RevealElementInput( + token: "some-token", label: "card number")) + XCTAssertNotNil(flowVaultLabel) + } + + // MARK: - Legacy-only client operations remain callable and validated + + func testLegacyOnlyOperationsStayAvailableWithBothImports() { + let legacyClient = makeLegacyClient() + + // getById exists only on the legacy SDK's Client extension, so the + // call is unambiguous even with both modules imported. Missing + // "records" key fails validation synchronously via the callback. + let expectation = expectation(description: "getById validation failure") + let callback = FailureExpectingCallback(expectation: expectation) + legacyClient.getById(records: [:], callback: callback) + + waitForExpectations(timeout: 1) + XCTAssertNotNil(callback.receivedError) + } +} From a452c30f5ab72b9c1f07ee316c905d79934269d0 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 17 Aug 2026 12:26:16 +0530 Subject: [PATCH 13/16] SK-3043 update link in readme for upgrade section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 557b50ee..7e80ed08 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This repository publishes two SDKs from a shared codebase. Pick the one that mat | **Skyflow** | PDB vault (v1 API) | `pod 'Skyflow'` / `Skyflow` | `import Skyflow` | [Skyflow/README.md](Skyflow/README.md) | | **SkyflowFlowVault** | Flow vault (v2 API, beta) | `pod 'SkyflowFlowVault'` / `SkyflowFlowVault` | `import SkyflowFlowVault` | [SkyflowFlowVault/README.md](SkyflowFlowVault/README.md) | -Not sure which one you need? If you are an existing Skyflow iOS customer, stay on **Skyflow** — it is fully backward compatible. **SkyflowFlowVault** is for Flow vaults and includes the migration guide for moving from PDB to FlowDB. +Not sure which one you need? If you are an existing Skyflow iOS customer, stay on **Skyflow** — it is fully backward compatible. **SkyflowFlowVault** is for Flow vaults; if you are moving from a PDB vault, follow its [Upgrading from PDB to FlowDB](SkyflowFlowVault/README.md#upgrading-from-pdb-to-flowdb) migration guide. ## Repository layout From 32bf25365884f96eead5d5f22cc621e0f6cd0dbb Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 17 Aug 2026 12:44:05 +0530 Subject: [PATCH 14/16] SK-3043 add tests for core folder sdk --- Package.swift | 7 +- .../CoreLogicTests.swift | 115 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 SkyflowCore/Tests/skyflow-iOS-coreTests/CoreLogicTests.swift diff --git a/Package.swift b/Package.swift index d0343733..de81b166 100644 --- a/Package.swift +++ b/Package.swift @@ -80,6 +80,11 @@ let package = Package( // public API only, the way an app installing both pods would. .testTarget(name: "skyflow-iOS-coexistenceTests", dependencies: ["Skyflow", "SkyflowFlowVault"], - path: "Tests/skyflow-iOS-coexistenceTests") + path: "Tests/skyflow-iOS-coexistenceTests"), + // Shared-core logic tests: depend only on SkyflowCore, so core + // regressions surface here independently of either SDK. + .testTarget(name: "skyflow-iOS-coreTests", + dependencies: ["SkyflowCore"], + path: "SkyflowCore/Tests/skyflow-iOS-coreTests") ] ) diff --git a/SkyflowCore/Tests/skyflow-iOS-coreTests/CoreLogicTests.swift b/SkyflowCore/Tests/skyflow-iOS-coreTests/CoreLogicTests.swift new file mode 100644 index 00000000..3960af1b --- /dev/null +++ b/SkyflowCore/Tests/skyflow-iOS-coreTests/CoreLogicTests.swift @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025 Skyflow +*/ + +// Tests for SkyflowCore's shared, contract-agnostic logic. This bundle +// depends ONLY on SkyflowCore — no SDK target — so it stays green (or red) +// independently of either SDK's setup, and it physically cannot grow a +// dependency on contract-specific types. + +import XCTest +@testable import SkyflowCore + +final class CoreLogicTests: XCTestCase { + // MARK: - CardType detection + + func testCardTypeDetection() { + XCTAssertEqual(CardType.forCardNumber(cardNumber: "4111111111111111"), CardType.VISA) + XCTAssertEqual(CardType.forCardNumber(cardNumber: "5555555555554444"), CardType.MASTERCARD) + XCTAssertEqual(CardType.forCardNumber(cardNumber: "378282246310005"), CardType.AMEX) + XCTAssertEqual(CardType.forCardNumber(cardNumber: "6011111111111117"), CardType.DISCOVER) + } + + // MARK: - Card number validation (Luhn) + + func testCardNumberValidator() { + // The regex is matched against the raw input (separators included) + // before the card-length and Luhn checks run. + let validator = SkyflowValidateCardNumber(error: "invalid card", regex: "^[0-9 -]+$") + + XCTAssertTrue(validator.validate("4111111111111111")) + // Separators are stripped before validating. + XCTAssertTrue(validator.validate("4111 1111 1111 1111")) + XCTAssertTrue(validator.validate("4111-1111-1111-1111")) + // Luhn checksum failure. + XCTAssertFalse(validator.validate("4111111111111112")) + // Documented behavior: empty input is not an error at this layer + // (emptiness is handled by the required-field check). + XCTAssertTrue(validator.validate("")) + } + + // MARK: - Expiration month validation + + func testExpirationMonthValidator() { + let validator = SkyflowValidateExpirationMonth(error: "invalid month") + + XCTAssertTrue(validator.validate("1")) + XCTAssertTrue(validator.validate("01")) + XCTAssertTrue(validator.validate("12")) + XCTAssertFalse(validator.validate("13")) + XCTAssertFalse(validator.validate("123")) + XCTAssertFalse(validator.validate("ab")) + XCTAssertTrue(validator.validate("")) + } + + // MARK: - Public validation rules + + func testLengthMatchRule() { + let rule = LengthMatchRule(minLength: 2, maxLength: 4, error: "bad length") + + XCTAssertTrue(rule.validate("ab")) + XCTAssertTrue(rule.validate("abcd")) + XCTAssertFalse(rule.validate("a")) + XCTAssertFalse(rule.validate("abcde")) + } + + func testRegexMatchRule() { + let rule = RegexMatchRule(regex: "^[0-9]+$", error: "digits only") + + XCTAssertTrue(rule.validate("12345")) + XCTAssertFalse(rule.validate("12a45")) + } + + // MARK: - CoreRequestValidators (shared pre-flight checks) + + private func message(_ errorCode: ErrorCodes) -> String { + return errorCode.getErrorObject(contextOptions: ContextOptions()).localizedDescription + } + + func testCheckClientConfig() { + XCTAssertNil(CoreRequestValidators.checkClientConfig( + vaultID: "vault", vaultURL: "https://example.org/v1/vaults/")) + + let emptyID = CoreRequestValidators.checkClientConfig(vaultID: "", vaultURL: "https://example.org/") + XCTAssertEqual(message(emptyID!), message(ErrorCodes.EMPTY_VAULT_ID())) + + // An empty vaultURL becomes "/" in SkyflowCore.Client. + let emptyURL = CoreRequestValidators.checkClientConfig(vaultID: "vault", vaultURL: "/") + XCTAssertEqual(message(emptyURL!), message(ErrorCodes.EMPTY_VAULT_URL())) + } + + func testCheckInsertRecordEntries() { + let valid: [[String: Any]] = [["table": "cards", "fields": ["cvv": "123"]]] + XCTAssertNil(CoreRequestValidators.checkInsertRecordEntries(valid)) + + let missingTable: [[String: Any]] = [["fields": ["cvv": "123"]]] + XCTAssertEqual(message(CoreRequestValidators.checkInsertRecordEntries(missingTable)!), + message(ErrorCodes.TABLE_KEY_ERROR(value: "0"))) + + let emptyTable: [[String: Any]] = [["table": "", "fields": ["cvv": "123"]]] + XCTAssertEqual(message(CoreRequestValidators.checkInsertRecordEntries(emptyTable)!), + message(ErrorCodes.EMPTY_TABLE_NAME())) + + let missingFields: [[String: Any]] = [["table": "cards"]] + XCTAssertEqual(message(CoreRequestValidators.checkInsertRecordEntries(missingFields)!), + message(ErrorCodes.FIELDS_KEY_ERROR(value: "0"))) + + let wrongFieldsType: [[String: Any]] = [["table": "cards", "fields": "not-a-dict"]] + XCTAssertEqual(message(CoreRequestValidators.checkInsertRecordEntries(wrongFieldsType)!), + message(ErrorCodes.INVALID_FIELDS_TYPE(value: "0"))) + + let emptyFields: [[String: Any]] = [["table": "cards", "fields": [String: Any]()]] + XCTAssertEqual(message(CoreRequestValidators.checkInsertRecordEntries(emptyFields)!), + message(ErrorCodes.EMPTY_FIELDS_KEY(value: "0"))) + } +} From 8efc7f71e02b34b9047a216849c2e95250685591 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 17 Aug 2026 18:57:34 +0530 Subject: [PATCH 15/16] SK-3043 update samples --- .../project.pbxproj | 12 +---- .../CollectAndRevealViewController.swift | 43 +++++++++++----- .../ExampleAPICallback.swift | 48 ------------------ .../ResponseStructs.swift | 23 --------- .../ExampleAPICallback.swift | 48 ------------------ .../ComposableElements/ResponseStructs.swift | 23 --------- .../ComposableElements/ViewController.swift | 43 +++++++++++----- .../InputFormatting.xcodeproj/project.pbxproj | 16 ------ .../InputFormatting/ExampleAPICallback.swift | 50 ------------------- .../InputFormatting/ResponseStructs.swift | 29 ----------- .../InputFormatting/ViewController.swift | 47 +++++++++++------ .../ExampleAPICallback.swift | 48 ------------------ .../ResponseStructs.swift | 24 --------- .../ViewController.swift | 15 ++++-- .../UpsertFeature.xcodeproj/project.pbxproj | 4 -- .../UpsertFeature/ResponseStructs.swift | 20 -------- .../UpsertFeature/ViewController.swift | 33 +++++++++--- .../ComposableContainerOperations.swift | 2 +- .../DemoImpl.swift | 6 +++ ...skyflow_iOS_composableEelementsTests.swift | 12 ++--- .../skyflow_iOS_composableFlowTests.swift | 8 +-- 21 files changed, 146 insertions(+), 408 deletions(-) delete mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift delete mode 100644 SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift delete mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift delete mode 100644 SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift delete mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift delete mode 100644 SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift delete mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift delete mode 100644 SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift delete mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj index 08155bf3..7178318f 100644 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6CAC82BEF157325DE8024E2E /* libPods-CollectAndRevealSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5C5B124A49D66E3C560B0D /* libPods-CollectAndRevealSample.a */; }; 7C2C89D82C89C9520076C641 /* CardBrandChoiceSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2C89D72C89C9520076C641 /* CardBrandChoiceSampleViewController.swift */; }; 7C2C89DE2C8F23C30076C641 /* CollectAndRevealViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2C89DD2C8F23C30076C641 /* CollectAndRevealViewController.swift */; }; 7C2C89E52C8F25C50076C641 /* libSkyflow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C2C89E42C8F25C50076C641 /* libSkyflow.a */; }; @@ -16,9 +17,7 @@ F1613E57272565CF0042C130 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1613E55272565CF0042C130 /* Main.storyboard */; }; F1613E59272565D30042C130 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F1613E58272565D30042C130 /* Assets.xcassets */; }; F1613E5C272565D30042C130 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1613E5A272565D30042C130 /* LaunchScreen.storyboard */; }; - F1613E652725663C0042C130 /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1613E642725663C0042C130 /* ExampleAPICallback.swift */; }; F1613E682725665D0042C130 /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1613E672725665D0042C130 /* ExampleTokenProvider.swift */; }; - F1613E6B272566800042C130 /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1613E6A272566800042C130 /* ResponseStructs.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -36,9 +35,7 @@ F1613E58272565D30042C130 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F1613E5B272565D30042C130 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F1613E5D272565D30042C130 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F1613E642725663C0042C130 /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; F1613E672725665D0042C130 /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; - F1613E6A272566800042C130 /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; F2D4C9A9B1D1DF56AC7781D1 /* Pods-CollectAndRevealSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CollectAndRevealSample.release.xcconfig"; path = "Target Support Files/Pods-CollectAndRevealSample/Pods-CollectAndRevealSample.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -48,6 +45,7 @@ buildActionMask = 2147483647; files = ( 7C2C89E52C8F25C50076C641 /* libSkyflow.a in Frameworks */, + 6CAC82BEF157325DE8024E2E /* libPods-CollectAndRevealSample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -93,9 +91,7 @@ F1613E58272565D30042C130 /* Assets.xcassets */, F1613E5A272565D30042C130 /* LaunchScreen.storyboard */, F1613E5D272565D30042C130 /* Info.plist */, - F1613E642725663C0042C130 /* ExampleAPICallback.swift */, F1613E672725665D0042C130 /* ExampleTokenProvider.swift */, - F1613E6A272566800042C130 /* ResponseStructs.swift */, 7C2C89DD2C8F23C30076C641 /* CollectAndRevealViewController.swift */, ); path = CollectAndRevealSample; @@ -128,8 +124,6 @@ dependencies = ( ); name = CollectAndRevealSample; - packageProductDependencies = ( - ); productName = CollectAndRevealSample; productReference = F1613E4C272565CF0042C130 /* CollectAndRevealSample.app */; productType = "com.apple.product-type.application"; @@ -230,10 +224,8 @@ buildActionMask = 2147483647; files = ( F1613E54272565CF0042C130 /* ViewController.swift in Sources */, - F1613E6B272566800042C130 /* ResponseStructs.swift in Sources */, F1613E682725665D0042C130 /* ExampleTokenProvider.swift in Sources */, F1613E50272565CF0042C130 /* AppDelegate.swift in Sources */, - F1613E652725663C0042C130 /* ExampleAPICallback.swift in Sources */, F1613E52272565CF0042C130 /* SceneDelegate.swift in Sources */, 7C2C89DE2C8F23C30076C641 /* CollectAndRevealViewController.swift in Sources */, 7C2C89D82C89C9520076C641 /* CardBrandChoiceSampleViewController.swift in Sources */, diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 39890a1b..69cde866 100644 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -153,26 +153,38 @@ class CollectAndRevealViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + self.revealContainer?.reveal( + callback: RevealCallback( + onSuccess: { (response: RevealResponse) in print("success:", response) }, + onFailure: { (error: SkyflowError) in print("failure:", error) } + ) + ) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + container!.collect( + callback: CollectCallback( + onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, + onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } + ), + options: SkyflowFlowVault.CollectOptions(tokens: true) + ) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + if let record = response.records.first { + updateRevealInputs(record: record) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: SkyflowError) { + if error.message.contains("Invalid Bearer token") && retryCount <= 2 { // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(record: CollectRecord) { let revealBaseStyle = SkyflowFlowVault.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -182,6 +194,11 @@ class CollectAndRevealViewController: UIViewController { textColor: UIColor.blue ) let revealStyles = SkyflowFlowVault.Styles(base: revealBaseStyle) + // A column maps to a list of Token(token:tokenGroupName:) - there's just one token + // group configured for these columns here, so take the first. + func token(for column: String) -> String { + record.tokens?[column]?.first?.token ?? "" + } DispatchQueue.main.async { if self.revealed { self.removeRevealElements() @@ -189,7 +206,7 @@ class CollectAndRevealViewController: UIViewController { self.revealed = true } let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( - token: tokens.card_number, + token: token(for: "card_number"), inputStyles: revealStyles, label: "Card Number", redaction: .REDACTED @@ -199,14 +216,14 @@ class CollectAndRevealViewController: UIViewController { options: SkyflowFlowVault.RevealElementOptions() ) let revealCVVtInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cvv, + token: token(for: "cvv"), inputStyles: revealStyles, label: "CVV", redaction: .MASKED ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cardholder_name, + token: token(for: "cardholder_name"), inputStyles: revealStyles, label: "Card Holder Name", redaction: .DEFAULT @@ -214,14 +231,14 @@ class CollectAndRevealViewController: UIViewController { ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( - token: tokens.expiry_month, + token: token(for: "expiry_month"), inputStyles: revealStyles, label: "Expiration Month", redaction: .PLAIN_TEXT ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( - token: tokens.expiry_year, + token: token(for: "expiry_year"), inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift deleted file mode 100644 index cfdf3deb..00000000 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import SkyflowFlowVault - -public class ExampleAPICallback: SkyflowFlowVault.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift deleted file mode 100644 index cfdf3deb..00000000 --- a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import SkyflowFlowVault - -public class ExampleAPICallback: SkyflowFlowVault.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift index cf71db43..e9955043 100644 --- a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -170,26 +170,43 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + self.revealContainer?.reveal( + callback: RevealCallback( + onSuccess: { (response: RevealResponse) in print("success:", response) }, + onFailure: { (error: SkyflowError) in print("failure:", error) } + ) + ) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + container!.collect( + callback: CollectCallback( + onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, + onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } + ), + options: SkyflowFlowVault.CollectOptions(tokens: true) + ) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + if let record = response.records.first { + updateRevealInputs(record: record) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: SkyflowError) { + if error.message.contains("Invalid Bearer token") && retryCount <= 2 { // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(record: CollectRecord) { + // A column maps to a list of Token(token:tokenGroupName:) - there's just one token + // group configured for these columns here, so take the first. + func token(for column: String) -> String { + record.tokens?[column]?.first?.token ?? "" + } let revealBaseStyle = SkyflowFlowVault.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -206,7 +223,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( - token: tokens.card_number, + token: token(for: "card_number"), inputStyles: revealStyles, label: "Card Number", redaction: .REDACTED @@ -216,14 +233,14 @@ class ViewController: UIViewController { options: SkyflowFlowVault.RevealElementOptions() ) let revealCVVtInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cvv, + token: token(for: "cvv"), inputStyles: revealStyles, label: "CVV", redaction: .MASKED ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cardholder_name, + token: token(for: "cardholder_name"), inputStyles: revealStyles, label: "Card Holder Name", redaction: .DEFAULT @@ -231,14 +248,14 @@ class ViewController: UIViewController { ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( - token: tokens.expiry_month, + token: token(for: "expiry_month"), inputStyles: revealStyles, label: "Expiration Month", redaction: .PLAIN_TEXT ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( - token: tokens.expiry_year, + token: token(for: "expiry_year"), inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj index 6605649c..4a079e02 100644 --- a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting.xcodeproj/project.pbxproj @@ -17,15 +17,9 @@ 7C4197BD29F978A9002CA73E /* InputFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197BC29F978A9002CA73E /* InputFormattingTests.swift */; }; 7C4197C729F978A9002CA73E /* InputFormattingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197C629F978A9002CA73E /* InputFormattingUITests.swift */; }; 7C4197C929F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197C829F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift */; }; - 7C4197D629F97E9F002CA73E /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */; }; - 7C4197D729F97E9F002CA73E /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */; }; - 7C4197D829F97E9F002CA73E /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */; }; 7C4197DA29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */; }; 7C4197DB29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */; }; 7C4197DC29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */; }; - 7C4197DE29F97EFD002CA73E /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */; }; - 7C4197DF29F97EFD002CA73E /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */; }; - 7C4197E029F97EFD002CA73E /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */; }; 8E4FC698E4F6F8422455BB76 /* Pods_InputFormatting_InputFormattingUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 362238A2034F5E696B5CB492 /* Pods_InputFormatting_InputFormattingUITests.framework */; }; 96007AFE98552781CD9CA0FB /* Pods_InputFormatting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15DB42C2155F11A364E2EA97 /* Pods_InputFormatting.framework */; }; /* End PBXBuildFile section */ @@ -66,9 +60,7 @@ 7C4197C229F978A9002CA73E /* InputFormattingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InputFormattingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7C4197C629F978A9002CA73E /* InputFormattingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFormattingUITests.swift; sourceTree = ""; }; 7C4197C829F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFormattingUITestsLaunchTests.swift; sourceTree = ""; }; - 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; - 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; B4544E89336067DF84CD3C85 /* Pods-InputFormatting.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormatting.debug.xcconfig"; path = "Target Support Files/Pods-InputFormatting/Pods-InputFormatting.debug.xcconfig"; sourceTree = ""; }; BD7E5EBA615A5DD2B93E86DB /* Pods-InputFormattingTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InputFormattingTests.release.xcconfig"; path = "Target Support Files/Pods-InputFormattingTests/Pods-InputFormattingTests.release.xcconfig"; sourceTree = ""; }; D28B0C4C19EA3040136B35C5 /* Pods_InputFormattingTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InputFormattingTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -135,9 +127,7 @@ 7C4197AE29F978A9002CA73E /* Assets.xcassets */, 7C4197B029F978A9002CA73E /* LaunchScreen.storyboard */, 7C4197B329F978A9002CA73E /* Info.plist */, - 7C4197D529F97E9F002CA73E /* ExampleAPICallback.swift */, 7C4197D929F97ED2002CA73E /* ExampleTokenProvider.swift */, - 7C4197DD29F97EFD002CA73E /* ResponseStructs.swift */, ); path = InputFormatting; sourceTree = ""; @@ -422,10 +412,8 @@ buildActionMask = 2147483647; files = ( 7C4197AA29F978A8002CA73E /* ViewController.swift in Sources */, - 7C4197DE29F97EFD002CA73E /* ResponseStructs.swift in Sources */, 7C4197DA29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */, 7C4197A629F978A8002CA73E /* AppDelegate.swift in Sources */, - 7C4197D629F97E9F002CA73E /* ExampleAPICallback.swift in Sources */, 7C4197A829F978A8002CA73E /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -434,10 +422,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7C4197D729F97E9F002CA73E /* ExampleAPICallback.swift in Sources */, 7C4197DB29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */, 7C4197BD29F978A9002CA73E /* InputFormattingTests.swift in Sources */, - 7C4197DF29F97EFD002CA73E /* ResponseStructs.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -445,10 +431,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7C4197E029F97EFD002CA73E /* ResponseStructs.swift in Sources */, 7C4197C929F978A9002CA73E /* InputFormattingUITestsLaunchTests.swift in Sources */, 7C4197DC29F97ED2002CA73E /* ExampleTokenProvider.swift in Sources */, - 7C4197D829F97E9F002CA73E /* ExampleAPICallback.swift in Sources */, 7C4197C729F978A9002CA73E /* InputFormattingUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift deleted file mode 100644 index 7cf1c0a0..00000000 --- a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ -// Created by Bharti Sagar on 26/04/23. -// - -import Foundation -import SkyflowFlowVault - -public class ExampleAPICallback: SkyflowFlowVault.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift deleted file mode 100644 index 2230b92f..00000000 --- a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ResponseStructs.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// Created by Bharti Sagar on 26/04/23. -// - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let ssn: String - let phone_number: String - let license_number: String - let skyflow_id: String -} diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift index 0616287e..2a3eb25f 100644 --- a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -201,21 +201,38 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + self.revealContainer?.reveal( + callback: RevealCallback( + onSuccess: { (response: RevealResponse) in print("success:", response) }, + onFailure: { (error: SkyflowError) in print("failure:", error) } + ) + ) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + container!.collect( + callback: CollectCallback( + onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, + onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } + ), + options: SkyflowFlowVault.CollectOptions(tokens: true) + ) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: CollectResponse) { print(response) - updateRevealInputs(tokens: response.records[0].fields) + if let record = response.records.first { + updateRevealInputs(record: record) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { + internal func updateFailure(error: SkyflowError) { print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(record: CollectRecord) { + // A column maps to a list of Token(token:tokenGroupName:) - there's just one token + // group configured for these columns here, so take the first. + func token(for column: String) -> String { + record.tokens?[column]?.first?.token ?? "" + } let revealBaseStyle = SkyflowFlowVault.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -232,7 +249,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( - token: tokens.card_number, + token: token(for: "card_number"), inputStyles: revealStyles, label: "Card Number" ) @@ -241,43 +258,43 @@ class ViewController: UIViewController { options: SkyflowFlowVault.RevealElementOptions(format: "XXXX-XXXX-XXXX-XXXX-XXX", translation: ["X": "[0-9]"]) ) let revealCVVtInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cvv, + token: token(for: "cvv"), inputStyles: revealStyles, label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cardholder_name, + token: token(for: "cardholder_name"), inputStyles: revealStyles, label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( - token: tokens.expiry_month, + token: token(for: "expiry_month"), inputStyles: revealStyles, label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( - token: tokens.expiry_year, + token: token(for: "expiry_year"), inputStyles: revealStyles, label: "Expiration Year" ) self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) let revealSSNInput = SkyflowFlowVault.RevealElementInput( - token: tokens.ssn, + token: token(for: "ssn"), inputStyles: revealStyles, label: "SSN" ) self.revealSSN = self.revealContainer?.create(input: revealSSNInput, options: SkyflowFlowVault.RevealElementOptions(format: "XXX XX XXXX", translation: ["X": "[0-9]"])) let revealPhoneNumberInput = SkyflowFlowVault.RevealElementInput( - token: tokens.phone_number, + token: token(for: "phone_number"), inputStyles: revealStyles, label: "Phone Number" ) self.revealPhoneNumber = self.revealContainer?.create(input: revealPhoneNumberInput, options: SkyflowFlowVault.RevealElementOptions(format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) let revealLicenseNumberInput = SkyflowFlowVault.RevealElementInput( - token: tokens.phone_number, + token: token(for: "phone_number"), inputStyles: revealStyles, label: "License Number" ) diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift deleted file mode 100644 index bd93892c..00000000 --- a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import SkyflowFlowVault - -public class ExampleAPICallback: SkyflowFlowVault.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (Any) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift deleted file mode 100644 index 4b948ce6..00000000 --- a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields? - let table: String - let skyflow_id: String? -} - -struct Fields: Codable { - let cardholder_name: String? - let card_number: String? - let expiry_month: String? - let expiry_year: String? - let cvv: String? - let skyflow_id: String? -} diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift index ebc6c24f..31ceed84 100644 --- a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -164,16 +164,21 @@ class ViewController: UIViewController { } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + container!.collect( + callback: CollectCallback( + onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, + onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } + ), + options: SkyflowFlowVault.CollectOptions(tokens: true) + ) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: CollectResponse) { print(response) retryCount = 0 print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: SkyflowError) { + if error.message.contains("Invalid Bearer token") && retryCount <= 2 { // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj index b06950f8..9387c88a 100644 --- a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 9818DE9F29239C17004C72AE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9818DE9D29239C17004C72AE /* Main.storyboard */; }; 9818DEA129239C1C004C72AE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9818DEA029239C1C004C72AE /* Assets.xcassets */; }; 9818DEA429239C1C004C72AE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9818DEA229239C1C004C72AE /* LaunchScreen.storyboard */; }; - 9818DEAC29239C87004C72AE /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEAB29239C87004C72AE /* ResponseStructs.swift */; }; 9818DEB029239CD0004C72AE /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */; }; 9818DEB229239CD9004C72AE /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */; }; 9818DEB82923B61D004C72AE /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 9818DEB72923B61D004C72AE /* Skyflow */; }; @@ -28,7 +27,6 @@ 9818DEA029239C1C004C72AE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9818DEA329239C1C004C72AE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 9818DEA529239C1C004C72AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9818DEAB29239C87004C72AE /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleTokenProvider.swift; path = ../../Upsert/Upsert/ExampleTokenProvider.swift; sourceTree = ""; }; 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleAPICallback.swift; path = ../../Upsert/Upsert/ExampleAPICallback.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -69,7 +67,6 @@ 9818DE9B29239C17004C72AE /* ViewController.swift */, 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */, 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */, - 9818DEAB29239C87004C72AE /* ResponseStructs.swift */, 9818DE9D29239C17004C72AE /* Main.storyboard */, 9818DEA029239C1C004C72AE /* Assets.xcassets */, 9818DEA229239C1C004C72AE /* LaunchScreen.storyboard */, @@ -159,7 +156,6 @@ 9818DE9829239C17004C72AE /* AppDelegate.swift in Sources */, 9818DEB229239CD9004C72AE /* ExampleAPICallback.swift in Sources */, 9818DE9A29239C17004C72AE /* SceneDelegate.swift in Sources */, - 9818DEAC29239C87004C72AE /* ResponseStructs.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift deleted file mode 100644 index 6ea1a8a6..00000000 --- a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardnumber: String - let cvv: String - let skyflow_id: String -} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift index 060519aa..3a971da6 100644 --- a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -150,17 +150,29 @@ class ViewController: UIViewController { } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + self.revealContainer?.reveal( + callback: RevealCallback( + onSuccess: { (response: RevealResponse) in print("success:", response) }, + onFailure: { (error: SkyflowError) in print("failure:", error) } + ) + ) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] - container!.collect(callback: exampleAPICallback, options: SkyflowFlowVault.CollectOptions(tokens: true)) + container!.collect( + callback: CollectCallback( + onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, + onFailure: { [weak self] (_: SkyflowError) in self?.updateFailure() } + ), + options: SkyflowFlowVault.CollectOptions(tokens: true) + ) } - internal func updateSuccess(_ response: SuccessResponse) { - updateRevealInputs(tokens: response.records[0].fields) + internal func updateSuccess(_ response: CollectResponse) { + if let record = response.records.first { + updateRevealInputs(record: record) + } print("Successfully got response:", response) } @@ -168,7 +180,12 @@ class ViewController: UIViewController { print("Failed Operation") } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(record: CollectRecord) { + // A column maps to a list of Token(token:tokenGroupName:) - there's just one token + // group configured for these columns here, so take the first. + func token(for column: String) -> String { + record.tokens?[column]?.first?.token ?? "" + } let revealBaseStyle = SkyflowFlowVault.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -185,7 +202,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cardnumber, + token: token(for: "cardnumber"), inputStyles: revealStyles, label: "Card Number", redaction: .DEFAULT @@ -195,7 +212,7 @@ class ViewController: UIViewController { options: SkyflowFlowVault.RevealElementOptions() ) let revealCvvInput = SkyflowFlowVault.RevealElementInput( - token: tokens.cvv, + token: token(for: "cvv"), inputStyles: revealStyles, label: "Cvv", redaction: .DEFAULT diff --git a/SkyflowFlowVault/Sources/composable/ComposableContainerOperations.swift b/SkyflowFlowVault/Sources/composable/ComposableContainerOperations.swift index c65c8c98..80fe508b 100644 --- a/SkyflowFlowVault/Sources/composable/ComposableContainerOperations.swift +++ b/SkyflowFlowVault/Sources/composable/ComposableContainerOperations.swift @@ -8,7 +8,7 @@ import Foundation import UIKit public extension Container { - func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { + func collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) where T: ComposableContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COMPOSABLE_CONTAINER if let errorCode = CoreRequestValidators.checkClientConfig(vaultID: self.skyflow.vaultID, vaultURL: self.skyflow.vaultURL) { diff --git a/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/DemoImpl.swift b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/DemoImpl.swift index 0f1c7e55..89d11af8 100644 --- a/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/DemoImpl.swift +++ b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/DemoImpl.swift @@ -65,4 +65,10 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // Composable Container.collect(callback:) requires the concrete CollectCallback type - + // this wraps self so existing DemoAPICallback call sites only need a one-word change. + var asCollectCallback: CollectCallback { + CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index 0253054b..a0f3d8a1 100644 --- a/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -211,7 +211,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -258,7 +258,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -288,7 +288,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -316,7 +316,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber INVALID_CARD_NUMBER\n") @@ -344,7 +344,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber triggered error\n") @@ -501,7 +501,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) diff --git a/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift index 4085a8fd..c63dd543 100644 --- a/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift +++ b/SkyflowFlowVault/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableFlowTests.swift @@ -32,7 +32,7 @@ final class skyflow_iOS_composableFlowTests: XCTestCase { let expectation = XCTestExpectation(description: "composable collect with unmounted element should fail") let callback = DemoAPICallback(expectation: expectation) - container!.collect(callback: callback) + container!.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNMOUNTED_COLLECT_ELEMENT(value: "name").description) @@ -45,7 +45,7 @@ final class skyflow_iOS_composableFlowTests: XCTestCase { let expectation = XCTestExpectation(description: "composable collect with empty additionalFields should fail") let callback = DemoAPICallback(expectation: expectation) - container!.collect(callback: callback, options: CollectOptions(additionalFields: AdditionalFields(records: []))) + container!.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: AdditionalFields(records: []))) wait(for: [expectation], timeout: 10.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_RECORDS_OBJECT().description) @@ -58,7 +58,7 @@ final class skyflow_iOS_composableFlowTests: XCTestCase { let expectation = XCTestExpectation(description: "composable collect with empty upsert should fail") let callback = DemoAPICallback(expectation: expectation) - container!.collect(callback: callback, options: CollectOptions(upsert: [])) + container!.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert: [])) wait(for: [expectation], timeout: 10.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().description) @@ -76,7 +76,7 @@ final class skyflow_iOS_composableFlowTests: XCTestCase { let expectation = XCTestExpectation(description: "composable collect should reach the network layer and fail offline") let callback = DemoAPICallback(expectation: expectation) - container!.collect(callback: callback, options: options) + container!.collect(callback: callback.asCollectCallback, options: options) wait(for: [expectation], timeout: 20.0) // The whole request-building path (validators, CollectRequestBuilder, CVV From 4aa8bd55f8f2cb695417d4d337c89e67707e96f0 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 17 Aug 2026 21:08:42 +0530 Subject: [PATCH 16/16] SK-3043 update samples --- .../UpsertFeature.xcodeproj/project.pbxproj | 4 +- .../UpsertFeature/ExampleAPICallback.swift | 48 +++++++++++++++++++ .../UpsertFeature/ExampleTokenProvider.swift | 31 ++++++++++++ .../UpsertFeature/ViewController.swift | 2 +- .../Validations.xcodeproj/project.pbxproj | 4 ++ .../Validations/ExampleAPICallback.swift | 26 ---------- .../Validations/ViewController.swift | 26 ---------- .../project.pbxproj | 6 --- .../CardBrandChoiceSampleViewController.swift | 2 +- .../CollectAndRevealViewController.swift | 32 ++++++------- .../project.pbxproj | 4 ++ .../ComposableElements/ViewController.swift | 38 +++++++-------- .../InputFormatting/ViewController.swift | 19 ++++---- .../ViewController.swift | 30 ++++++------ .../UpsertFeature.xcodeproj/project.pbxproj | 6 +-- .../UpsertFeature/ExampleTokenProvider.swift | 31 ++++++++++++ .../UpsertFeature/ViewController.swift | 21 ++++---- .../Validations.xcodeproj/project.pbxproj | 4 ++ .../Validations/ExampleAPICallback.swift | 26 ---------- .../Validations/ViewController.swift | 27 ----------- 20 files changed, 193 insertions(+), 194 deletions(-) create mode 100644 Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleAPICallback.swift create mode 100644 Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift delete mode 100644 Skyflow/Samples/Validations/Validations/ExampleAPICallback.swift create mode 100644 SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift delete mode 100644 SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj index b06950f8..47bf5f75 100644 --- a/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj @@ -29,8 +29,8 @@ 9818DEA329239C1C004C72AE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 9818DEA529239C1C004C72AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9818DEAB29239C87004C72AE /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; - 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleTokenProvider.swift; path = ../../Upsert/Upsert/ExampleTokenProvider.swift; sourceTree = ""; }; - 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleAPICallback.swift; path = ../../Upsert/Upsert/ExampleAPICallback.swift; sourceTree = ""; }; + 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; + 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleAPICallback.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleAPICallback.swift new file mode 100644 index 00000000..6d0bbb21 --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleAPICallback.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleAPICallback: Skyflow.Callback { + private var updateSuccess: ((SuccessResponse) -> Void)? + private var updateFailure: (() -> Void)? + private var decoder: JSONDecoder? + + internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping () -> Void) { + self.updateSuccess = updateSuccess + self.updateFailure = updateFailure + self.decoder = JSONDecoder() + } + + internal init() { + } + + public func onSuccess(_ responseBody: Any) { + if updateSuccess == nil { + print("success:", responseBody) + return + } + do { + let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) + let responseData = Data(dataString!.utf8) + let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) + + updateSuccess!(jsonResponse) + + print("success:", jsonResponse) + } catch { + print("error deserializing", error) + } + } + + public func onFailure(_ error: Any) { + print(error) + if let updateFailure = updateFailure { + updateFailure() + return + } + print("onfailure", error) + } +} diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift new file mode 100644 index 00000000..75a0efee --- /dev/null +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import Skyflow + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: Skyflow.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift index 86f38902..9c1b6925 100644 --- a/Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Skyflow/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -40,7 +40,7 @@ class ViewController: UIViewController { textAlignment: .left, textColor: .blue ) - let focusStyle = Skyflow.Style(borderColor: .blue) ̰ + let focusStyle = Skyflow.Style(borderColor: .blue) let completedStyle = Skyflow.Style(borderColor: UIColor.green, textColor: UIColor.green) let invalidStyle = Skyflow.Style(borderColor: UIColor.red, textColor: UIColor.red) let styles = Skyflow.Styles( diff --git a/Skyflow/Samples/Validations/Validations.xcodeproj/project.pbxproj b/Skyflow/Samples/Validations/Validations.xcodeproj/project.pbxproj index afc0c387..ba4de380 100644 --- a/Skyflow/Samples/Validations/Validations.xcodeproj/project.pbxproj +++ b/Skyflow/Samples/Validations/Validations.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 965D96070B64549FC9188C80 /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C27061F816370F5D626963B /* ExampleTokenProvider.swift */; }; F1030006274E0A0D008F614B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030005274E0A0D008F614B /* AppDelegate.swift */; }; F1030008274E0A0D008F614B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030007274E0A0D008F614B /* SceneDelegate.swift */; }; F103000A274E0A0D008F614B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030009274E0A0D008F614B /* ViewController.swift */; }; @@ -16,6 +17,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 7C27061F816370F5D626963B /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; F1030002274E0A0D008F614B /* Validations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Validations.app; sourceTree = BUILT_PRODUCTS_DIR; }; F1030005274E0A0D008F614B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F1030007274E0A0D008F614B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -55,6 +57,7 @@ F103000E274E0A12008F614B /* Assets.xcassets */, F1030010274E0A12008F614B /* LaunchScreen.storyboard */, F1030013274E0A12008F614B /* Info.plist */, + 7C27061F816370F5D626963B /* ExampleTokenProvider.swift */, ); path = Validations; sourceTree = ""; @@ -140,6 +143,7 @@ F103000A274E0A0D008F614B /* ViewController.swift in Sources */, F1030006274E0A0D008F614B /* AppDelegate.swift in Sources */, F1030008274E0A0D008F614B /* SceneDelegate.swift in Sources */, + 965D96070B64549FC9188C80 /* ExampleTokenProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Skyflow/Samples/Validations/Validations/ExampleAPICallback.swift b/Skyflow/Samples/Validations/Validations/ExampleAPICallback.swift deleted file mode 100644 index 18703254..00000000 --- a/Skyflow/Samples/Validations/Validations/ExampleAPICallback.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// -// ExampleAPICallback.swift -// Validations -// -// Created by Akhil Anil Mangala on 24/11/21. -// - -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - print("success:", responseBody) - } - - public func onFailure(_ error: Any) { - print("failure:", error) - } -} diff --git a/Skyflow/Samples/Validations/Validations/ViewController.swift b/Skyflow/Samples/Validations/Validations/ViewController.swift index b2da5958..e791f493 100644 --- a/Skyflow/Samples/Validations/Validations/ViewController.swift +++ b/Skyflow/Samples/Validations/Validations/ViewController.swift @@ -13,8 +13,6 @@ class ViewController: UIViewController { private var skyflowClient: Skyflow.Client? private var container: Skyflow.Container? private var b: UIButton? - private var confirmPasswordElement: Skyflow.TextField? - private var stackView: UIStackView! override func loadView() { @@ -99,15 +97,9 @@ class ViewController: UIViewController { validations: ValidationSet(rules: [strongPasswordRule, lengthRule, elementValueMatchRule]) ) let confirmPassword = container?.create(input: confirmPasswordInput, options: collectElementOptions) - self.confirmPasswordElement = confirmPassword // mount elements on screen - errors will be shown if any of the validaitons fail stackView.addArrangedSubview(password!) stackView.addArrangedSubview(confirmPassword!) - let resetButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) - resetButton.backgroundColor = .blue - resetButton.setTitle("Reset Password", for: .normal) - resetButton.addTarget(self, action: #selector(resetPassword), for: .touchUpInside) - stackView.addArrangedSubview(resetButton) stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 10 @@ -119,22 +111,4 @@ class ViewController: UIViewController { stackView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10).isActive = true } } - - @objc func resetPassword() { - let url = "" - let requestHeaders = ["Content-Type": "application/json ", "Authorization": ""] - let requestBody: [String: Any] = [ - // Other fields... - "password": self.confirmPasswordElement! - ] - let responseBody: [String: Any] = [:] - let connectionConfig = ConnectionConfig( - connectionURL: url, - method: .POST, - requestBody: requestBody, - requestHeader: requestHeaders, - responseBody: responseBody - ) - self.skyflowClient?.invokeConnection(config: connectionConfig, callback: ExampleAPICallback()) - } } diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj index 7178318f..413c3382 100644 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 6CAC82BEF157325DE8024E2E /* libPods-CollectAndRevealSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5C5B124A49D66E3C560B0D /* libPods-CollectAndRevealSample.a */; }; 7C2C89D82C89C9520076C641 /* CardBrandChoiceSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2C89D72C89C9520076C641 /* CardBrandChoiceSampleViewController.swift */; }; 7C2C89DE2C8F23C30076C641 /* CollectAndRevealViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2C89DD2C8F23C30076C641 /* CollectAndRevealViewController.swift */; }; - 7C2C89E52C8F25C50076C641 /* libSkyflow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C2C89E42C8F25C50076C641 /* libSkyflow.a */; }; F1613E50272565CF0042C130 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1613E4F272565CF0042C130 /* AppDelegate.swift */; }; F1613E52272565CF0042C130 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1613E51272565CF0042C130 /* SceneDelegate.swift */; }; F1613E54272565CF0042C130 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1613E53272565CF0042C130 /* ViewController.swift */; }; @@ -24,8 +23,6 @@ 267485426163B1ACCD8ED25D /* Pods-CollectAndRevealSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CollectAndRevealSample.debug.xcconfig"; path = "Target Support Files/Pods-CollectAndRevealSample/Pods-CollectAndRevealSample.debug.xcconfig"; sourceTree = ""; }; 7C2C89D72C89C9520076C641 /* CardBrandChoiceSampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBrandChoiceSampleViewController.swift; sourceTree = ""; }; 7C2C89DD2C8F23C30076C641 /* CollectAndRevealViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectAndRevealViewController.swift; sourceTree = ""; }; - 7C2C89E22C8F25BC0076C641 /* libSkyflow.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libSkyflow.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C2C89E42C8F25C50076C641 /* libSkyflow.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libSkyflow.a; sourceTree = BUILT_PRODUCTS_DIR; }; 7F5C5B124A49D66E3C560B0D /* libPods-CollectAndRevealSample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-CollectAndRevealSample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F1613E4C272565CF0042C130 /* CollectAndRevealSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CollectAndRevealSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; F1613E4F272565CF0042C130 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -44,7 +41,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7C2C89E52C8F25C50076C641 /* libSkyflow.a in Frameworks */, 6CAC82BEF157325DE8024E2E /* libPods-CollectAndRevealSample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -55,8 +51,6 @@ A94C3CD833597CC577C83A7A /* Frameworks */ = { isa = PBXGroup; children = ( - 7C2C89E42C8F25C50076C641 /* libSkyflow.a */, - 7C2C89E22C8F25BC0076C641 /* libSkyflow.a */, 7F5C5B124A49D66E3C560B0D /* libPods-CollectAndRevealSample.a */, ); name = Frameworks; diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift index aad636c3..0318b9c6 100644 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CardBrandChoiceSampleViewController.swift @@ -39,7 +39,7 @@ class CardBrandChoiceSampleViewController: UIViewController { let iconStyles = SkyflowFlowVault.Styles(base: Style(cardIconAlignment: .right)) // create card number element - let collectCardNumberInput = SkyflowFlowVault.CollectElementInput(table: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: SkyflowFlowVault.ElementType.CARD_NUMBER) + let collectCardNumberInput = SkyflowFlowVault.CollectElementInput(tableName: "", column: "", inputStyles: inputStyles, iconStyles: Styles(base: Style(cardIconAlignment: .right)),label: "Card Number", placeholder: "XXXXXXXXXXXXX", type: SkyflowFlowVault.ElementType.CARD_NUMBER) let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true, enableCardIcon: true, enableCopy: false) collectCardNumberElement = container?.create(input: collectCardNumberInput, options: requiredOption) diff --git a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 69cde866..0969252c 100644 --- a/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/SkyflowFlowVault/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -10,7 +10,6 @@ class CollectAndRevealViewController: UIViewController { private var skyflow: SkyflowFlowVault.Client? private var container: SkyflowFlowVault.Container? private var revealContainer: SkyflowFlowVault.Container? - private var b: UIButton? private var stackView: UIStackView! @@ -23,7 +22,6 @@ class CollectAndRevealViewController: UIViewController { private var revealed = false - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white @@ -67,7 +65,7 @@ class CollectAndRevealViewController: UIViewController { ) let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", @@ -75,7 +73,7 @@ class CollectAndRevealViewController: UIViewController { type: SkyflowFlowVault.ElementType.CARD_NUMBER ) let collectNameInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -83,7 +81,7 @@ class CollectAndRevealViewController: UIViewController { type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME ) let collectCVVInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", @@ -91,7 +89,7 @@ class CollectAndRevealViewController: UIViewController { type: .CVV ) let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -99,7 +97,7 @@ class CollectAndRevealViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -157,7 +155,10 @@ class CollectAndRevealViewController: UIViewController { callback: RevealCallback( onSuccess: { (response: RevealResponse) in print("success:", response) }, onFailure: { (error: SkyflowError) in print("failure:", error) } - ) + ), + options: SkyflowFlowVault.RevealOptions(tokenGroupRedactions: [ + SkyflowFlowVault.TokenGroupRedaction(tokenGroupName: "", redaction: "") + ]) ) } @objc func submitForm() { @@ -166,7 +167,7 @@ class CollectAndRevealViewController: UIViewController { onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } ), - options: SkyflowFlowVault.CollectOptions(tokens: true) + options: SkyflowFlowVault.CollectOptions() ) } internal func updateSuccess(_ response: CollectResponse) { @@ -208,8 +209,7 @@ class CollectAndRevealViewController: UIViewController { let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( token: token(for: "card_number"), inputStyles: revealStyles, - label: "Card Number", - redaction: .REDACTED + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, @@ -218,23 +218,19 @@ class CollectAndRevealViewController: UIViewController { let revealCVVtInput = SkyflowFlowVault.RevealElementInput( token: token(for: "cvv"), inputStyles: revealStyles, - label: "CVV", - redaction: .MASKED + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = SkyflowFlowVault.RevealElementInput( token: token(for: "cardholder_name"), inputStyles: revealStyles, - label: "Card Holder Name", - redaction: .DEFAULT - + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( token: token(for: "expiry_month"), inputStyles: revealStyles, - label: "Expiration Month", - redaction: .PLAIN_TEXT + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj index 19f868a0..7f0fb353 100644 --- a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 67D8C91FAB59F1B52D6AAD3E /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51739068CD4F85D3D45E427 /* ExampleTokenProvider.swift */; }; 7C5748C72A7B71DF002D64AA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748C62A7B71DF002D64AA /* AppDelegate.swift */; }; 7C5748C92A7B71DF002D64AA /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748C82A7B71DF002D64AA /* SceneDelegate.swift */; }; 7C5748CB2A7B71DF002D64AA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5748CA2A7B71DF002D64AA /* ViewController.swift */; }; @@ -49,6 +50,7 @@ 7C5748E32A7B71DF002D64AA /* ComposableElementsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ComposableElementsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7C5748E72A7B71DF002D64AA /* ComposableElementsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposableElementsUITests.swift; sourceTree = ""; }; 7C5748E92A7B71DF002D64AA /* ComposableElementsUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposableElementsUITestsLaunchTests.swift; sourceTree = ""; }; + B51739068CD4F85D3D45E427 /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -106,6 +108,7 @@ 7C5748CF2A7B71DF002D64AA /* Assets.xcassets */, 7C5748D12A7B71DF002D64AA /* LaunchScreen.storyboard */, 7C5748D42A7B71DF002D64AA /* Info.plist */, + B51739068CD4F85D3D45E427 /* ExampleTokenProvider.swift */, ); path = ComposableElements; sourceTree = ""; @@ -261,6 +264,7 @@ 7C5748CB2A7B71DF002D64AA /* ViewController.swift in Sources */, 7C5748C72A7B71DF002D64AA /* AppDelegate.swift in Sources */, 7C5748C92A7B71DF002D64AA /* SceneDelegate.swift in Sources */, + 67D8C91FAB59F1B52D6AAD3E /* ExampleTokenProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift index e9955043..8c21925d 100644 --- a/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/SkyflowFlowVault/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -8,9 +8,8 @@ import SkyflowFlowVault class ViewController: UIViewController { var retryCount = 0 private var skyflow: SkyflowFlowVault.Client? - private var container: SkyflowFlowVault.Container? + private var container: SkyflowFlowVault.Container? private var revealContainer: SkyflowFlowVault.Container? - private var b: UIButton? private var stackView: UIStackView! @@ -73,7 +72,7 @@ class ViewController: UIViewController { ) let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", @@ -81,7 +80,7 @@ class ViewController: UIViewController { type: SkyflowFlowVault.ElementType.CARD_NUMBER ) let collectNameInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -89,7 +88,7 @@ class ViewController: UIViewController { type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME ) let collectCVVInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", @@ -97,7 +96,7 @@ class ViewController: UIViewController { type: .CVV ) let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -105,7 +104,7 @@ class ViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -129,8 +128,9 @@ class ViewController: UIViewController { revealButton.addTarget(self, action: #selector(revealForm), for: .touchUpInside) do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) + if let composableView = try container?.getComposableView() { + stackView.addArrangedSubview(composableView) + } } catch { print(error) } @@ -174,7 +174,10 @@ class ViewController: UIViewController { callback: RevealCallback( onSuccess: { (response: RevealResponse) in print("success:", response) }, onFailure: { (error: SkyflowError) in print("failure:", error) } - ) + ), + options: SkyflowFlowVault.RevealOptions(tokenGroupRedactions: [ + SkyflowFlowVault.TokenGroupRedaction(tokenGroupName: "", redaction: "") + ]) ) } @objc func submitForm() { @@ -183,7 +186,7 @@ class ViewController: UIViewController { onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } ), - options: SkyflowFlowVault.CollectOptions(tokens: true) + options: SkyflowFlowVault.CollectOptions() ) } internal func updateSuccess(_ response: CollectResponse) { @@ -225,8 +228,7 @@ class ViewController: UIViewController { let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( token: token(for: "card_number"), inputStyles: revealStyles, - label: "Card Number", - redaction: .REDACTED + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, @@ -235,23 +237,19 @@ class ViewController: UIViewController { let revealCVVtInput = SkyflowFlowVault.RevealElementInput( token: token(for: "cvv"), inputStyles: revealStyles, - label: "CVV", - redaction: .MASKED + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = SkyflowFlowVault.RevealElementInput( token: token(for: "cardholder_name"), inputStyles: revealStyles, - label: "Card Holder Name", - redaction: .DEFAULT - + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = SkyflowFlowVault.RevealElementInput( token: token(for: "expiry_month"), inputStyles: revealStyles, - label: "Expiration Month", - redaction: .PLAIN_TEXT + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = SkyflowFlowVault.RevealElementInput( diff --git a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift index 2a3eb25f..0c497182 100644 --- a/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift +++ b/SkyflowFlowVault/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -11,7 +11,6 @@ class ViewController: UIViewController { private var skyflow: SkyflowFlowVault.Client? private var container: SkyflowFlowVault.Container? private var revealContainer: SkyflowFlowVault.Container? - private var b: UIButton? private var stackView: UIStackView! @@ -77,7 +76,7 @@ class ViewController: UIViewController { ) let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "card_number", inputStyles: styles, label: "Card Number", @@ -85,7 +84,7 @@ class ViewController: UIViewController { type: SkyflowFlowVault.ElementType.CARD_NUMBER ) let collectNameInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", @@ -93,7 +92,7 @@ class ViewController: UIViewController { type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME ) let collectCVVInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "cvv", inputStyles: styles, label: "CVV", @@ -101,7 +100,7 @@ class ViewController: UIViewController { type: .CVV ) let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "expiry_month", inputStyles: styles, label: "Expiration Month", @@ -109,7 +108,7 @@ class ViewController: UIViewController { type: .EXPIRATION_MONTH ) let collectExpYearInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "expiry_year", inputStyles: styles, label: "Expiration Year", @@ -117,7 +116,7 @@ class ViewController: UIViewController { type: .EXPIRATION_YEAR ) let collectSSNInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "ssn", inputStyles: styles, label: "SSN", @@ -125,7 +124,7 @@ class ViewController: UIViewController { type: .INPUT_FIELD ) let collectPhoneNumberInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "phone_number", inputStyles: styles, label: "Phone Number", @@ -133,7 +132,7 @@ class ViewController: UIViewController { type: .INPUT_FIELD ) let collectLicenseNumberInput = SkyflowFlowVault.CollectElementInput( - table: "table", + tableName: "table", column: "license_number", inputStyles: styles, label: "License Number", @@ -214,7 +213,7 @@ class ViewController: UIViewController { onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } ), - options: SkyflowFlowVault.CollectOptions(tokens: true) + options: SkyflowFlowVault.CollectOptions() ) } internal func updateSuccess(_ response: CollectResponse) { diff --git a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift index 31ceed84..87c1e691 100644 --- a/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift +++ b/SkyflowFlowVault/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -8,8 +8,7 @@ import SkyflowFlowVault class ViewController: UIViewController { var retryCount = 0 private var skyflow: SkyflowFlowVault.Client? - private var container: SkyflowFlowVault.Container? - private var b: UIButton? + private var container: SkyflowFlowVault.Container? private var stackView: UIStackView! @@ -63,51 +62,51 @@ class ViewController: UIViewController { ) let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "card_number", inputStyles: styles, label: "Card Number", placeholder: "4111-1111-1111-1111", type: SkyflowFlowVault.ElementType.CARD_NUMBER, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input ) let collectNameInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cardholder_name", inputStyles: styles, label: "Card Holder Name", placeholder: "John Doe", type: SkyflowFlowVault.ElementType.CARDHOLDER_NAME, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input ) let collectCVVInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "cvv", inputStyles: styles, label: "CVV", placeholder: "***", type: .CVV, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input ) let collectExpMonthInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_month", inputStyles: styles, label: "Expiration Month", placeholder: "MM", type: .EXPIRATION_MONTH, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input ) let collectExpYearInput = SkyflowFlowVault.CollectElementInput( - table: "credit_cards", + tableName: "credit_cards", column: "expiry_year", inputStyles: styles, label: "Expiration Year", placeholder: "YYYY", type: .EXPIRATION_YEAR, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input ) let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) @@ -122,8 +121,9 @@ class ViewController: UIViewController { collectButton.addTarget(self, action: #selector(submitForm), for: .touchUpInside) do { - let composableView = try container?.getComposableView() - stackView.addArrangedSubview(composableView) + if let composableView = try container?.getComposableView() { + stackView.addArrangedSubview(composableView) + } } catch { print(error) } @@ -169,7 +169,7 @@ class ViewController: UIViewController { onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, onFailure: { [weak self] (error: SkyflowError) in self?.updateFailure(error: error) } ), - options: SkyflowFlowVault.CollectOptions(tokens: true) + options: SkyflowFlowVault.CollectOptions() ) } internal func updateSuccess(_ response: CollectResponse) { diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj index 9387c88a..23f1b1d3 100644 --- a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 9818DEA129239C1C004C72AE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9818DEA029239C1C004C72AE /* Assets.xcassets */; }; 9818DEA429239C1C004C72AE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9818DEA229239C1C004C72AE /* LaunchScreen.storyboard */; }; 9818DEB029239CD0004C72AE /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */; }; - 9818DEB229239CD9004C72AE /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */; }; 9818DEB82923B61D004C72AE /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 9818DEB72923B61D004C72AE /* Skyflow */; }; /* End PBXBuildFile section */ @@ -27,8 +26,7 @@ 9818DEA029239C1C004C72AE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9818DEA329239C1C004C72AE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 9818DEA529239C1C004C72AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleTokenProvider.swift; path = ../../Upsert/Upsert/ExampleTokenProvider.swift; sourceTree = ""; }; - 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleAPICallback.swift; path = ../../Upsert/Upsert/ExampleAPICallback.swift; sourceTree = ""; }; + 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -65,7 +63,6 @@ 9818DE9729239C17004C72AE /* AppDelegate.swift */, 9818DE9929239C17004C72AE /* SceneDelegate.swift */, 9818DE9B29239C17004C72AE /* ViewController.swift */, - 9818DEB129239CD9004C72AE /* ExampleAPICallback.swift */, 9818DEAF29239CD0004C72AE /* ExampleTokenProvider.swift */, 9818DE9D29239C17004C72AE /* Main.storyboard */, 9818DEA029239C1C004C72AE /* Assets.xcassets */, @@ -154,7 +151,6 @@ 9818DE9C29239C17004C72AE /* ViewController.swift in Sources */, 9818DEB029239CD0004C72AE /* ExampleTokenProvider.swift in Sources */, 9818DE9829239C17004C72AE /* AppDelegate.swift in Sources */, - 9818DEB229239CD9004C72AE /* ExampleAPICallback.swift in Sources */, 9818DE9A29239C17004C72AE /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift new file mode 100644 index 00000000..a170d9fe --- /dev/null +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ExampleTokenProvider.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Skyflow + */ + +import Foundation +import SkyflowFlowVault + +public class ExampleTokenProvider: TokenProvider { + public func getBearerToken(_ apiCallback: SkyflowFlowVault.Callback) { + if let url = URL(string: "") { + let session = URLSession(configuration: .default) + let task = session.dataTask(with: url) { data, _, error in + if error != nil { + print(error!) + return + } + if let safeData = data { + do { + let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] + if let accessToken = x?["accessToken"] { + apiCallback.onSuccess(accessToken) + } + } catch { + print("access token wrong format") + } + } + } + task.resume() + } + } +} diff --git a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift index 3a971da6..ce1ab57f 100644 --- a/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/SkyflowFlowVault/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -9,7 +9,6 @@ class ViewController: UIViewController { private var skyflow: SkyflowFlowVault.Client? private var container: SkyflowFlowVault.Container? private var revealContainer: SkyflowFlowVault.Container? - private var b: UIButton? private var stackView: UIStackView! private var revealCardNumber: Label? private var revealCvv: Label? @@ -40,7 +39,7 @@ class ViewController: UIViewController { textAlignment: .left, textColor: .blue ) - let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) ̰ + let focusStyle = SkyflowFlowVault.Style(borderColor: .blue) let completedStyle = SkyflowFlowVault.Style(borderColor: UIColor.green, textColor: UIColor.green) let invalidStyle = SkyflowFlowVault.Style(borderColor: UIColor.red, textColor: UIColor.red) let styles = SkyflowFlowVault.Styles( @@ -85,7 +84,7 @@ class ViewController: UIViewController { ) // keep card number as unique column for testing upsert feature let collectCardNumberInput = SkyflowFlowVault.CollectElementInput( - table: "persons", + tableName: "persons", column: "cardnumber", inputStyles: styles, labelStyles: labelStyles, @@ -97,7 +96,7 @@ class ViewController: UIViewController { let requiredOption = SkyflowFlowVault.CollectElementOptions(required: true) let collectCardNumber = container?.create(input: collectCardNumberInput, options: requiredOption) let collectCvvInput = SkyflowFlowVault.CollectElementInput( - table: "persons", + tableName: "persons", column: "cvv", inputStyles: styles, label: "Cvv", @@ -154,18 +153,20 @@ class ViewController: UIViewController { callback: RevealCallback( onSuccess: { (response: RevealResponse) in print("success:", response) }, onFailure: { (error: SkyflowError) in print("failure:", error) } - ) + ), + options: SkyflowFlowVault.RevealOptions(tokenGroupRedactions: [ + SkyflowFlowVault.TokenGroupRedaction(tokenGroupName: "", redaction: "") + ]) ) } @objc func submitForm() { - let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] container!.collect( callback: CollectCallback( onSuccess: { [weak self] (response: CollectResponse) in self?.updateSuccess(response) }, onFailure: { [weak self] (_: SkyflowError) in self?.updateFailure() } ), - options: SkyflowFlowVault.CollectOptions(tokens: true) + options: SkyflowFlowVault.CollectOptions() ) } @@ -204,8 +205,7 @@ class ViewController: UIViewController { let revealCardNumberInput = SkyflowFlowVault.RevealElementInput( token: token(for: "cardnumber"), inputStyles: revealStyles, - label: "Card Number", - redaction: .DEFAULT + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, @@ -214,8 +214,7 @@ class ViewController: UIViewController { let revealCvvInput = SkyflowFlowVault.RevealElementInput( token: token(for: "cvv"), inputStyles: revealStyles, - label: "Cvv", - redaction: .DEFAULT + label: "Cvv" ) self.revealCvv = self.revealContainer?.create( input: revealCvvInput, diff --git a/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj index afc0c387..0fdc1230 100644 --- a/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj +++ b/SkyflowFlowVault/Samples/Validations/Validations.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 61BA6DDEE43670E58D668441 /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47B16B92A54F283C62D4E02 /* ExampleTokenProvider.swift */; }; F1030006274E0A0D008F614B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030005274E0A0D008F614B /* AppDelegate.swift */; }; F1030008274E0A0D008F614B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030007274E0A0D008F614B /* SceneDelegate.swift */; }; F103000A274E0A0D008F614B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1030009274E0A0D008F614B /* ViewController.swift */; }; @@ -24,6 +25,7 @@ F103000E274E0A12008F614B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F1030011274E0A12008F614B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F1030013274E0A12008F614B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F47B16B92A54F283C62D4E02 /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,6 +57,7 @@ F103000E274E0A12008F614B /* Assets.xcassets */, F1030010274E0A12008F614B /* LaunchScreen.storyboard */, F1030013274E0A12008F614B /* Info.plist */, + F47B16B92A54F283C62D4E02 /* ExampleTokenProvider.swift */, ); path = Validations; sourceTree = ""; @@ -140,6 +143,7 @@ F103000A274E0A0D008F614B /* ViewController.swift in Sources */, F1030006274E0A0D008F614B /* AppDelegate.swift in Sources */, F1030008274E0A0D008F614B /* SceneDelegate.swift in Sources */, + 61BA6DDEE43670E58D668441 /* ExampleTokenProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift b/SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift deleted file mode 100644 index 48f4c77a..00000000 --- a/SkyflowFlowVault/Samples/Validations/Validations/ExampleAPICallback.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -// -// ExampleAPICallback.swift -// Validations -// -// Created by Akhil Anil Mangala on 24/11/21. -// - -import Foundation -import SkyflowFlowVault - -public class ExampleAPICallback: SkyflowFlowVault.Callback { - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - print("success:", responseBody) - } - - public func onFailure(_ error: Any) { - print("failure:", error) - } -} diff --git a/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift b/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift index 972262ff..3064b205 100644 --- a/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift +++ b/SkyflowFlowVault/Samples/Validations/Validations/ViewController.swift @@ -12,9 +12,6 @@ import SkyflowFlowVault class ViewController: UIViewController { private var skyflowClient: SkyflowFlowVault.Client? private var container: SkyflowFlowVault.Container? - private var b: UIButton? - private var confirmPasswordElement: SkyflowFlowVault.TextField? - private var stackView: UIStackView! override func loadView() { @@ -99,15 +96,9 @@ class ViewController: UIViewController { validations: ValidationSet(rules: [strongPasswordRule, lengthRule, elementValueMatchRule]) ) let confirmPassword = container?.create(input: confirmPasswordInput, options: collectElementOptions) - self.confirmPasswordElement = confirmPassword // mount elements on screen - errors will be shown if any of the validaitons fail stackView.addArrangedSubview(password!) stackView.addArrangedSubview(confirmPassword!) - let resetButton = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 40)) - resetButton.backgroundColor = .blue - resetButton.setTitle("Reset Password", for: .normal) - resetButton.addTarget(self, action: #selector(resetPassword), for: .touchUpInside) - stackView.addArrangedSubview(resetButton) stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 10 @@ -119,22 +110,4 @@ class ViewController: UIViewController { stackView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10).isActive = true } } - - @objc func resetPassword() { - let url = "" - let requestHeaders = ["Content-Type": "application/json ", "Authorization": ""] - let requestBody: [String: Any] = [ - // Other fields... - "password": self.confirmPasswordElement! - ] - let responseBody: [String: Any] = [:] - let connectionConfig = ConnectionConfig( - connectionURL: url, - method: .POST, - requestBody: requestBody, - requestHeader: requestHeaders, - responseBody: responseBody - ) - self.skyflowClient?.invokeConnection(config: connectionConfig, callback: ExampleAPICallback()) - } }