diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore new file mode 100644 index 000000000..4463b2f73 --- /dev/null +++ b/mobile/ios/.gitignore @@ -0,0 +1,15 @@ +# Xcode +xcuserdata/ +*.xcuserstate +DerivedData/ +.swiftpm/ +*.xcworkspace/xcuserdata/ + +.build/ + +# copy Secrets.swift.sample to Secrets.swift +PayabliPayInDemo/Secrets.swift + +# copy .env.template to .env +TokenServer/.env + diff --git a/mobile/ios/PayabliPayInDemo.xcodeproj/project.pbxproj b/mobile/ios/PayabliPayInDemo.xcodeproj/project.pbxproj new file mode 100644 index 000000000..d757b9756 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo.xcodeproj/project.pbxproj @@ -0,0 +1,371 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 09B76223C6587A6E43CB7C79 /* PayInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9491D8034F0A6469925703B7 /* PayInView.swift */; }; + 10FDE57F6EF31BB0FFBEAED1 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81123013B86A8321213FF86 /* Config.swift */; }; + 23E8FAA85AACF0446F59FFC0 /* PayabliSDKTapToPay in Frameworks */ = {isa = PBXBuildFile; productRef = 49B0AA752E3642C71244D787 /* PayabliSDKTapToPay */; }; + 472BE6CA3523BB9FB5DD9F2C /* PayabliPayInDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65E64443B00BAC165594A5E /* PayabliPayInDemoApp.swift */; }; + 4EECC265349DAB8D7C68C625 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2F21B1B60DC58D27AFA0A /* HomeView.swift */; }; + 73FE1CE983D2FC19D82B4718 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2367C9421E1223C6ACBB7A02 /* Assets.xcassets */; }; + 88981D280AD74AE04EE0D21E /* TapToPayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDD90FF2016D08618F838E3 /* TapToPayView.swift */; }; + B2678971592B5FB91C16DA7F /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CBC63827295CA73076D97 /* Secrets.swift */; }; + C5985F746D1F12661DC618C4 /* PayabliSDKPayInPaymentFlow in Frameworks */ = {isa = PBXBuildFile; productRef = 88D12BA42A6F67C93718F0B7 /* PayabliSDKPayInPaymentFlow */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05E01371277B76ECB6E289A2 /* PayabliPayInDemo.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PayabliPayInDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2367C9421E1223C6ACBB7A02 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3DDD90FF2016D08618F838E3 /* TapToPayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayView.swift; sourceTree = ""; }; + 508CBC63827295CA73076D97 /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; + 5CB2F21B1B60DC58D27AFA0A /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 9491D8034F0A6469925703B7 /* PayInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayInView.swift; sourceTree = ""; }; + 9FAC212636EE23273321EA6C /* PayabliPayInDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PayabliPayInDemo.entitlements; sourceTree = ""; }; + C65E64443B00BAC165594A5E /* PayabliPayInDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayabliPayInDemoApp.swift; sourceTree = ""; }; + E81123013B86A8321213FF86 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + F51DDF822FBCA3A25EEF227C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 45504EC63B6F5C6852213DD7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 23E8FAA85AACF0446F59FFC0 /* PayabliSDKTapToPay in Frameworks */, + C5985F746D1F12661DC618C4 /* PayabliSDKPayInPaymentFlow in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 56105375DC94549AC4B98BED /* PayabliPayInDemo */ = { + isa = PBXGroup; + children = ( + 2367C9421E1223C6ACBB7A02 /* Assets.xcassets */, + E81123013B86A8321213FF86 /* Config.swift */, + 5CB2F21B1B60DC58D27AFA0A /* HomeView.swift */, + F51DDF822FBCA3A25EEF227C /* Info.plist */, + 9FAC212636EE23273321EA6C /* PayabliPayInDemo.entitlements */, + C65E64443B00BAC165594A5E /* PayabliPayInDemoApp.swift */, + 9491D8034F0A6469925703B7 /* PayInView.swift */, + 508CBC63827295CA73076D97 /* Secrets.swift */, + 3DDD90FF2016D08618F838E3 /* TapToPayView.swift */, + ); + path = PayabliPayInDemo; + sourceTree = ""; + }; + E80106BECD97D21AB823C56C /* Products */ = { + isa = PBXGroup; + children = ( + 05E01371277B76ECB6E289A2 /* PayabliPayInDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + EAD80E0B2DB89AAEFDE2F840 = { + isa = PBXGroup; + children = ( + 56105375DC94549AC4B98BED /* PayabliPayInDemo */, + E80106BECD97D21AB823C56C /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 899FA68AFA6B20596CB2562E /* PayabliPayInDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1F6009C6D4CAEC05E9D1882D /* Build configuration list for PBXNativeTarget "PayabliPayInDemo" */; + buildPhases = ( + A5683E9A954945052838835B /* Sources */, + B52ABE471435EEEB106C8647 /* Resources */, + 45504EC63B6F5C6852213DD7 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PayabliPayInDemo; + packageProductDependencies = ( + 49B0AA752E3642C71244D787 /* PayabliSDKTapToPay */, + 88D12BA42A6F67C93718F0B7 /* PayabliSDKPayInPaymentFlow */, + ); + productName = PayabliPayInDemo; + productReference = 05E01371277B76ECB6E289A2 /* PayabliPayInDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B97B9DA84EB0C5B4533F0EC7 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + }; + }; + buildConfigurationList = 038323704F2FE81005151738 /* Build configuration list for PBXProject "PayabliPayInDemo" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = EAD80E0B2DB89AAEFDE2F840; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 5BE3CC060278383346175D5D /* XCRemoteSwiftPackageReference "sdk-ios" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = E80106BECD97D21AB823C56C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 899FA68AFA6B20596CB2562E /* PayabliPayInDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B52ABE471435EEEB106C8647 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 73FE1CE983D2FC19D82B4718 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A5683E9A954945052838835B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 10FDE57F6EF31BB0FFBEAED1 /* Config.swift in Sources */, + 4EECC265349DAB8D7C68C625 /* HomeView.swift in Sources */, + 09B76223C6587A6E43CB7C79 /* PayInView.swift in Sources */, + 472BE6CA3523BB9FB5DD9F2C /* PayabliPayInDemoApp.swift in Sources */, + B2678971592B5FB91C16DA7F /* Secrets.swift in Sources */, + 88981D280AD74AE04EE0D21E /* TapToPayView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 00665B1CD3F097F0755DC77A /* 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 = 16.7; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 41CF0EB5E8460A7A637D8383 /* 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 = ( + "$(inherited)", + "DEBUG=1", + ); + 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.7; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + AA0106DA2EB0D6B19E66078D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = PayabliPayInDemo/PayabliPayInDemo.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = PayabliPayInDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.payabli.examples.payinDemo; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + F41A9F899305FA5635099304 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = PayabliPayInDemo/PayabliPayInDemo.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = PayabliPayInDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.payabli.examples.payinDemo; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 038323704F2FE81005151738 /* Build configuration list for PBXProject "PayabliPayInDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41CF0EB5E8460A7A637D8383 /* Debug */, + 00665B1CD3F097F0755DC77A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 1F6009C6D4CAEC05E9D1882D /* Build configuration list for PBXNativeTarget "PayabliPayInDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F41A9F899305FA5635099304 /* Debug */, + AA0106DA2EB0D6B19E66078D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5BE3CC060278383346175D5D /* XCRemoteSwiftPackageReference "sdk-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/payabli/sdk-ios.git"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 49B0AA752E3642C71244D787 /* PayabliSDKTapToPay */ = { + isa = XCSwiftPackageProductDependency; + package = 5BE3CC060278383346175D5D /* XCRemoteSwiftPackageReference "sdk-ios" */; + productName = PayabliSDKTapToPay; + }; + 88D12BA42A6F67C93718F0B7 /* PayabliSDKPayInPaymentFlow */ = { + isa = XCSwiftPackageProductDependency; + package = 5BE3CC060278383346175D5D /* XCRemoteSwiftPackageReference "sdk-ios" */; + productName = PayabliSDKPayInPaymentFlow; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = B97B9DA84EB0C5B4533F0EC7 /* Project object */; +} diff --git a/mobile/ios/PayabliPayInDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/PayabliPayInDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile/ios/PayabliPayInDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/PayabliPayInDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..f7834029c --- /dev/null +++ b/mobile/ios/PayabliPayInDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b4238d9834f3620405700a5165624cd0c60158e8a992757a33fdab28e4a3ad9a", + "pins" : [ + { + "identity" : "sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/payabli/sdk-ios.git", + "state" : { + "branch" : "main", + "revision" : "5804af4c2179cc36fe7905e2083c38a8d57e15ea" + } + } + ], + "version" : 3 +} diff --git a/mobile/ios/PayabliPayInDemo.xcodeproj/xcshareddata/xcschemes/PayabliPayInDemo.xcscheme b/mobile/ios/PayabliPayInDemo.xcodeproj/xcshareddata/xcschemes/PayabliPayInDemo.xcscheme new file mode 100644 index 000000000..705c16c77 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo.xcodeproj/xcshareddata/xcschemes/PayabliPayInDemo.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/PayabliPayInDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/ios/PayabliPayInDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..0afb3cf0e --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors": [ + { + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/mobile/ios/PayabliPayInDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/PayabliPayInDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..b121e3bce --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/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/mobile/ios/PayabliPayInDemo/Assets.xcassets/Contents.json b/mobile/ios/PayabliPayInDemo/Assets.xcassets/Contents.json new file mode 100644 index 000000000..74d6a722c --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/mobile/ios/PayabliPayInDemo/Config.swift b/mobile/ios/PayabliPayInDemo/Config.swift new file mode 100644 index 000000000..15302b558 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/Config.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Non-secret example configuration shared by both demo tabs. Values that +/// vary per developer live in `Secrets.swift` instead (copy it from +/// `Secrets.swift.sample`; it's gitignored). +enum Config { + /// Demo payment amount charged by both the Pay In and Tap to Pay tabs. + static let demoAmount: Double = 9.99 + + /// Prints redacted PayIn request/response diagnostics to the Xcode + /// console — invaluable when a capture fails without much detail in + /// the on-screen error. Turn off to quiet the console. + static let payInDiagnosticsEnabled = true +} diff --git a/mobile/ios/PayabliPayInDemo/HomeView.swift b/mobile/ios/PayabliPayInDemo/HomeView.swift new file mode 100644 index 000000000..06a09eebd --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/HomeView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +/// Landing screen: one tab per SDK feature covered by this example. +struct HomeView: View { + var body: some View { + TabView { + PayInView() + .tabItem { + Label("Pay In", systemImage: "creditcard") + } + + TapToPayView() + .tabItem { + Label("Tap to Pay", systemImage: "wave.3.right") + } + } + } +} diff --git a/mobile/ios/PayabliPayInDemo/Info.plist b/mobile/ios/PayabliPayInDemo/Info.plist new file mode 100644 index 000000000..74e7426cf --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/mobile/ios/PayabliPayInDemo/PayInView.swift b/mobile/ios/PayabliPayInDemo/PayInView.swift new file mode 100644 index 000000000..fe195afdd --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/PayInView.swift @@ -0,0 +1,125 @@ +import PayabliSDKPayInPaymentFlow +import SwiftUI + +/// Demonstrates `PayabliPayInPaymentFlow` configured for `.capture`: the +/// hosted SwiftUI form collects card or ACH details and submits a v2 +/// MoneyIn payment directly. Clear PAN data never reaches this view — +/// the SDK owns the sensitive fields and the network request. +struct PayInView: View { + @EnvironmentObject private var paymentFlow: PayabliPayInPaymentFlow + + @State private var resultText = "" + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + PayabliPayInPaymentFlowView( + component: paymentFlow, + configuration: configuration, + onCompleted: handleCompleted, + onError: handleError + ) + .payabliPayInPaymentFlowStyle(style) + + Text(resultText.isEmpty ? "No payment yet" : resultText) + .font(.footnote) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .padding(16) + } + .navigationTitle("Pay In") + } + } + + private var configuration: PayabliPayInPaymentFlowFormConfiguration { + PayabliPayInPaymentFlowFormConfiguration( + allowedMethods: [.card, .ach], + defaultMethod: .card, + // .capture submits a MoneyIn payment, which requires customer + // info (name/email) in addition to the payment method — without + // it the API rejects the request with "Error in customer data". + cardFieldOrder: [ + .cardholderName, + .cardNumber, + .cardExpiration, + .cardCvv, + .cardZip, + .firstName, + .lastName, + .billingEmail + ], + achFieldOrder: [ + .achHolder, + .achRouting, + .achAccount, + .achAccountType, + .firstName, + .lastName, + .billingEmail + ], + cardSections: [ + PayabliPayInPaymentFlowFieldSection( + title: "Card Information", + fields: [.cardholderName, .cardNumber, .cardExpiration, .cardCvv, .cardZip] + ), + PayabliPayInPaymentFlowFieldSection( + title: "Customer Information", + fields: [.firstName, .lastName, .billingEmail] + ) + ], + achSections: [ + PayabliPayInPaymentFlowFieldSection( + title: "Bank Information", + fields: [.achHolder, .achRouting, .achAccount, .achAccountType] + ), + PayabliPayInPaymentFlowFieldSection( + title: "Customer Information", + fields: [.firstName, .lastName, .billingEmail] + ) + ], + hiddenValues: PayabliPayInPaymentFlowHiddenValues( + achHolderType: .personal, + achSecCode: .web, + methodDescription: "Payabli iOS example payment" + ), + labels: PayabliPayInPaymentFlowLabels( + title: "Checkout", + subtitle: "Pay $\(Config.demoAmount) with a card or bank account.", + submitButton: "Pay $\(Config.demoAmount)" + ), + labelLayout: .external, + formatting: PayabliPayInPaymentFlowFormatting( + insertsCardNumberSpaces: true, + masksACHAccountEntry: true + ) + ) + } + + private var style: PayabliPayInPaymentFlowStyle { + PayabliPayInPaymentFlowStyle( + input: PayabliPayInPaymentFlowInputStyle( + backgroundColor: Color(.systemBackground), + borderColor: Color(.separator).opacity(0.6), + cornerRadius: 8 + ), + submitButton: PayabliPayInPaymentFlowSubmitButtonStyle(cornerRadius: 8) + ) + } + + private func handleCompleted(_ result: PayabliPayInPaymentFlowResult) { + resultText = [ + "Status: \(result.code)", + "Payment trans ID: \(result.transaction?.paymentTransId ?? "-")", + "Method: \(result.transaction?.method ?? "-")" + ].joined(separator: "\n") + } + + private func handleError(_ error: Error) { + resultText = "Payment failed: \(error.localizedDescription)" + } +} diff --git a/mobile/ios/PayabliPayInDemo/PayabliPayInDemo.entitlements b/mobile/ios/PayabliPayInDemo/PayabliPayInDemo.entitlements new file mode 100644 index 000000000..cfe3c73f9 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/PayabliPayInDemo.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.proximity-reader.payment.acceptance + + com.apple.developer.devicecheck.appattest-environment + development + + diff --git a/mobile/ios/PayabliPayInDemo/PayabliPayInDemoApp.swift b/mobile/ios/PayabliPayInDemo/PayabliPayInDemoApp.swift new file mode 100644 index 000000000..627d442c1 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/PayabliPayInDemoApp.swift @@ -0,0 +1,77 @@ +import PayabliSDKCore +import PayabliSDKPayInPaymentFlow +import PayabliSDKTapToPay +import SwiftUI + +/// Entry point of the demo app. +/// +/// Owns one `PayabliTTP` instance for Tap to Pay and one +/// `PayabliPayInPaymentFlow` instance configured to capture a card/ACH +/// payment, and hands both to `HomeView` as environment objects. +@main +struct PayabliPayInDemoApp: App { + // Just satisfies PayabliTTP's synchronous `accessToken:` parameter at + // launch — the SDK replaces it via `tokenProvider` on the first 401, + // so this never needs to be a real token. + private static let placeholderAccessToken = "placeholder-token" + + @StateObject private var ttp = PayabliTTP( + accessToken: placeholderAccessToken, + tokenProvider: { try await Secrets.fetchAccessToken() }, + entryPoint: Secrets.entryPoint, + appId: Secrets.appId, + environment: .sandbox + ) + + @StateObject private var payInPaymentFlow = PayabliPayInPaymentFlow( + entryPoint: Secrets.entryPoint, + environment: .sandbox, + accessTokenProvider: { try await Secrets.fetchAccessToken() }, + diagnostics: Self.diagnostics, + operation: .capture, + requestConfiguration: PayabliPayInPaymentFlowRequestConfiguration( + paymentDetails: PayabliPayInPaymentFlowPaymentDetails( + totalAmount: Config.demoAmount, + currency: "USD" + ), + // Sandbox paypoints configure which field(s) identify a + // customer under Settings > Custom Fields > Custom + // Identifiers — this one requires customerNumber. That's + // paypoint-specific, not part of the hosted form's visible + // fields, so it's supplied here; it merges with whatever + // the form collects (form values win where both are set). + customerData: PayabliPayInPaymentFlowCustomerData( + customerNumber: "ios-example-customer" + ), + orderDescription: "Payabli iOS example payment", + source: "ios-example-app" + ) + ) + + var body: some Scene { + WindowGroup { + HomeView() + .environmentObject(ttp) + .environmentObject(payInPaymentFlow) + } + } + + /// Prints the raw (redacted) request/response for every PayIn call — + /// check Xcode's console when a capture fails with a generic message. + private static var diagnostics: PayabliPayInPaymentFlowDiagnostics { + guard Config.payInDiagnosticsEnabled else { return .disabled } + return .enabled { entry in + var lines = [ + "[PayabliPayInPaymentFlowDiagnostics] \(entry.phase.rawValue.uppercased()) \(entry.method) \(entry.url)" + ] + if let statusCode = entry.statusCode { lines.append("statusCode=\(statusCode)") } + if let durationMilliseconds = entry.durationMilliseconds { + lines.append("durationMilliseconds=\(durationMilliseconds)") + } + lines.append("headers=\(entry.headers)") + if let body = entry.body { lines.append("body=\(body)") } + if let errorDescription = entry.errorDescription { lines.append("error=\(errorDescription)") } + print(lines.joined(separator: "\n")) + } + } +} diff --git a/mobile/ios/PayabliPayInDemo/Secrets.swift.sample b/mobile/ios/PayabliPayInDemo/Secrets.swift.sample new file mode 100644 index 000000000..be8c11fda --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/Secrets.swift.sample @@ -0,0 +1,39 @@ +import Foundation + +/// Copy this file to `Secrets.swift` and fill in `entryPoint` and `appId`. +/// `Secrets.swift` is gitignored so these never get committed. This is the +/// one file the app itself needs edited — everything else (sandbox API +/// credentials) lives server-side in `TokenServer/.env` instead, because a +/// Payabli `clientSecret` must never ship inside a mobile app binary: +/// anyone who downloads the app can decompile it and extract it. The local +/// server under `TokenServer/` stands in for that backend exchange — see +/// `TokenServer/README.md` to start it. +enum Secrets { + /// Partner integration identifier (platform `entryPoint`), the path + /// segment in `https://app-sandbox.payabli.com//signin`. + static let entryPoint = "" + + /// The host app's bundle identifier prefixed with the Apple Developer + /// Team ID (e.g. `"ABCDE12345.com.your.app"`). Tap to Pay forwards this + /// to App Attest during device attestation, and it must be authorized + /// on the entrypoint's device allowlist (Settings → Devices → + /// Authorized Apps in the Payabli dashboard). Only needed for the Tap + /// to Pay tab — leave the placeholder to just try Pay In. + static let appId = "." + + /// The local token server started via `cd TokenServer && npm start`. + /// Point this at your own backend in a real integration. On a physical + /// device this must be your Mac's LAN IP, not 127.0.0.1 — see + /// `TokenServer/README.md`. + private static let tokenServerURL = URL(string: "http://127.0.0.1:8787/payabli/access-token")! + + /// Fetches a fresh Payabli access token from the token server. + static func fetchAccessToken() async throws -> String { + let (data, response) = try await URLSession.shared.data(from: tokenServerURL) + guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else { + throw URLError(.badServerResponse) + } + struct TokenResponse: Decodable { let accessToken: String } + return try JSONDecoder().decode(TokenResponse.self, from: data).accessToken + } +} diff --git a/mobile/ios/PayabliPayInDemo/TapToPayView.swift b/mobile/ios/PayabliPayInDemo/TapToPayView.swift new file mode 100644 index 000000000..355ec84e0 --- /dev/null +++ b/mobile/ios/PayabliPayInDemo/TapToPayView.swift @@ -0,0 +1,153 @@ +import PayabliSDKTapToPay +import SwiftUI + +/// Demonstrates the Tap to Pay lifecycle: attest the device and prepare +/// the NFC reader with `ttp.initialize()`, then accept a contactless +/// card or wallet payment with `ttp.charge(type:paymentDetails:)`. +/// +/// Tap to Pay only works on a physical iPhone XS or newer running iOS +/// 16.7+ with the Tap to Pay entitlement — see `README.md`. On the +/// Simulator, `initialize()` fails at the eligibility gate. +struct TapToPayView: View { + @EnvironmentObject private var ttp: PayabliTTP + + @State private var lastResult = "" + @State private var eventLog: [EventLogEntry] = [] + @State private var eventToken: PayabliTTPEventToken? + @State private var isWorking = false + + var body: some View { + NavigationView { + List { + Section("Lifecycle") { + Button("Initialize") { runInitialize() } + .disabled(isWorking || ttp.isReady) + } + + Section("Charge") { + Button("Charge $\(Config.demoAmount)") { runCharge() } + .disabled(isWorking || !ttp.isReady) + } + + Section("Last result") { + Text(lastResult.isEmpty ? "—" : lastResult) + .font(.footnote) + .foregroundColor(.secondary) + } + + Section("Event log") { + if eventLog.isEmpty { + Text("No events yet") + .font(.footnote) + .foregroundColor(.secondary) + } else { + ForEach(eventLog) { entry in + Text(entry.label) + .font(.footnote.monospaced()) + .foregroundColor(.secondary) + } + } + } + } + .navigationTitle("Tap to Pay") + .toolbar { sessionBadge } + .onAppear(perform: subscribeToEvents) + .onDisappear { eventToken?.cancel() } + } + } + + private var sessionBadge: some ToolbarContent { + ToolbarItem(placement: .navigationBarTrailing) { + Text(stateLabel(ttp.sessionState)) + .font(.caption.bold()) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(stateColor(ttp.sessionState).opacity(0.2)) + .foregroundColor(stateColor(ttp.sessionState)) + .clipShape(Capsule()) + } + } + + // MARK: - Actions + + private func runInitialize() { + isWorking = true + Task { + defer { isWorking = false } + do { + try await ttp.initialize() + lastResult = "✓ Initialized" + } catch { + lastResult = "✗ Initialize failed: \(error.localizedDescription)" + } + } + } + + private func runCharge() { + isWorking = true + Task { + defer { isWorking = false } + do { + let result = try await ttp.charge( + type: .sale, + paymentDetails: PayabliTTPPaymentDetails(amount: Decimal(Config.demoAmount)) + ) + lastResult = "✓ Charged · txn \(result.paymentTransId)" + } catch { + lastResult = "✗ Charge failed: \(error.localizedDescription)" + } + } + } + + // MARK: - Event subscription + + private func subscribeToEvents() { + guard eventToken == nil else { return } + // The addEventListener token owns both the subscription and its + // tear-down (cancelled in onDisappear) — a plain `events()` async + // sequence has no handle SwiftUI could cancel from onDisappear. + eventToken = ttp.addEventListener { code, payload in + let detail = payload + .map { "\($0.key): \($0.value)" } + .sorted() + .joined(separator: ", ") + DispatchQueue.main.async { + eventLog.insert(EventLogEntry(label: "\(code)" + (detail.isEmpty ? "" : " — \(detail)")), at: 0) + if eventLog.count > 50 { + eventLog.removeLast(eventLog.count - 50) + } + } + } + } + + // MARK: - Cosmetics + + private func stateLabel(_ state: PayabliTTPSessionState) -> String { + switch state { + case .idle: return "idle" + case .attestingDevice: return "attesting" + case .fetchingConfig: return "config" + case .initializingReader: return "reader" + case .ready: return "ready" + case .sessionExpired: return "expired" + case .reinitializing: return "reinit" + case .pendingActivation: return "pending" + case .error: return "error" + @unknown default: return "?" + } + } + + private func stateColor(_ state: PayabliTTPSessionState) -> Color { + switch state { + case .ready: return .green + case .error, .sessionExpired: return .red + case .pendingActivation: return .orange + default: return .gray + } + } +} + +private struct EventLogEntry: Identifiable { + let id = UUID() + let label: String +} diff --git a/mobile/ios/README.md b/mobile/ios/README.md new file mode 100644 index 000000000..a49cea95d --- /dev/null +++ b/mobile/ios/README.md @@ -0,0 +1,63 @@ +# iOS Example App + +Demonstrates [Payabli's iOS SDK](https://github.com/payabli/sdk-ios): card/ACH payments with `PayabliPayInPaymentFlow`, and Tap to Pay on iPhone with `PayabliTTP`. + +## What's here + +- **Pay In tab** — hosted checkout form; submits a real payment. +- **Tap to Pay tab** — initialize the reader, then charge a contactless card or wallet. + +## Requirements + +- Xcode 15+ +- A Payabli sandbox entrypoint +- A physical iPhone XS+ on iOS 16.7+ for Tap to Pay (Pay In works in the Simulator) + +## Setup + +```bash +./run.sh +``` + +Run it. First time, it creates `TokenServer/.env` and `Secrets.swift` and stops so you can fill them in. Run it again and it starts the token server, builds the app, and launches it in the Simulator. + +For Tap to Pay, also set `appId` in `Secrets.swift` and authorize the app on your [entrypoint's device allowlist](https://github.com/payabli/sdk-ios#authorized-application-on-the-paypoint-allowlist), then run on a physical iPhone. + +Never put a `clientSecret` in the app itself — a compiled app can be decompiled. That's why it lives in `TokenServer/.env` instead, and the app fetches a token from that local server. + +### By hand, instead of `run.sh` + +1. `cd TokenServer && cp .env.template .env` — fill in `CLIENT_ID`/`CLIENT_SECRET` (or `PAYABLI_ACCESS_TOKEN`), then `npm start`. +2. `cd ../PayabliPayInDemo && cp Secrets.swift.sample Secrets.swift` — fill in `entryPoint`. +3. Open `PayabliPayInDemo.xcodeproj` in Xcode, run the `PayabliPayInDemo` scheme. + +### Regenerating the Xcode project + +```bash +xcodegen generate +``` + +Run this after changing `project.yml` or adding/removing source files. + +### Known issue + +Linking both `PayabliSDKTapToPay` and `PayabliSDKPayInPaymentFlow` hits a Swift Package Manager limitation — they share a `PayabliSDKCore` dependency that Xcode can't reconcile between two products in one target. Already worked around in `project.yml` (`DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC: YES`), so there's nothing to do — but worth flagging to the SDK team, since any app wanting both capabilities hits this. + +## Layout + +``` +mobile/ios/ +├── run.sh +├── PayabliPayInDemo.xcodeproj # generated — see project.yml +├── project.yml +├── PayabliPayInDemo/ +│ ├── PayabliPayInDemoApp.swift +│ ├── HomeView.swift +│ ├── PayInView.swift +│ ├── TapToPayView.swift +│ ├── Config.swift +│ └── Secrets.swift.sample +└── TokenServer/ +``` + +More: [SDK docs](https://github.com/payabli/sdk-ios), [PayIn overview](https://github.com/payabli/sdk-ios/blob/main/Documentation/PayInPaymentFlowOverview.md). diff --git a/mobile/ios/TokenServer/.env.template b/mobile/ios/TokenServer/.env.template new file mode 100644 index 000000000..2d48eabdc --- /dev/null +++ b/mobile/ios/TokenServer/.env.template @@ -0,0 +1,3 @@ +CLIENT_ID= +CLIENT_SECRET= +PAYABLI_API_BASE_URL=https://api-sandbox.payabli.com/api diff --git a/mobile/ios/TokenServer/README.md b/mobile/ios/TokenServer/README.md new file mode 100644 index 000000000..0ff5f6983 --- /dev/null +++ b/mobile/ios/TokenServer/README.md @@ -0,0 +1,41 @@ +# Local Token Server + +Stands in for a partner backend: it mints a Payabli access token so the app never embeds a `clientSecret`. See the [top-level README](../README.md) for how this fits in. + +## Setup + +```bash +cp .env.template .env +``` + +Fill in `.env` — either: +- `CLIENT_ID` / `CLIENT_SECRET`, exchanged automatically for a token, or +- `PAYABLI_ACCESS_TOKEN`, a sandbox token you already have (simpler, but expires and needs replacing). + +Then: + +```bash +npm start +``` + +The Simulator reaches it at `http://127.0.0.1:8787/payabli/access-token`. + +## Physical device + +A physical iPhone can't reach `127.0.0.1` on your Mac — use your Mac's LAN IP instead. Update `tokenServerURL` in `Secrets.swift`: + +```text +http://:8787/payabli/access-token +``` + +Bind the server to all interfaces: + +```bash +PAYABLI_LOCAL_TOKEN_SERVER_HOST=0.0.0.0 npm start +``` + +Trusted networks only, and stop the server when you're done. + +## Contract + +`GET /payabli/access-token` → `{ "accessToken": "..." }`. `Secrets.fetchAccessToken()` in the app expects exactly that. diff --git a/mobile/ios/TokenServer/package.json b/mobile/ios/TokenServer/package.json new file mode 100644 index 000000000..5bca4f564 --- /dev/null +++ b/mobile/ios/TokenServer/package.json @@ -0,0 +1,11 @@ +{ + "name": "payabli-mobile-ios-token-server", + "private": true, + "type": "module", + "scripts": { + "start": "node server.mjs" + }, + "engines": { + "node": ">=18" + } +} diff --git a/mobile/ios/TokenServer/server.mjs b/mobile/ios/TokenServer/server.mjs new file mode 100644 index 000000000..6c51b926a --- /dev/null +++ b/mobile/ios/TokenServer/server.mjs @@ -0,0 +1,128 @@ +import { createServer } from "node:http"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const serverDir = dirname(fileURLToPath(import.meta.url)); +loadEnv(join(serverDir, ".env")); + +const port = Number.parseInt(process.env.PORT || "8787", 10); +const bindHost = process.env.PAYABLI_LOCAL_TOKEN_SERVER_HOST || "127.0.0.1"; +const allowedApiHosts = new Set( + (process.env.PAYABLI_ALLOWED_API_HOSTS || "api-sandbox.payabli.com,api-qa.payabli.com,api.payabli.com") + .split(",") + .map((host) => host.trim().toLowerCase()) + .filter(Boolean) +); + +let cachedToken = null; // { token, expiresAt } + +const server = createServer((req, res) => { + handleRequest(req, res).catch((error) => { + console.error(redact(error.stack || error.message)); + sendJson(res, 500, { error: "Local token server failed", detail: redact(error.message) }); + }); +}); + +async function handleRequest(req, res) { + const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); + + if (url.pathname === "/health" && req.method === "GET") { + sendJson(res, 200, { ok: true }); + return; + } + + if (url.pathname === "/payabli/access-token" && req.method === "GET") { + sendJson(res, 200, { accessToken: await resolveAccessToken() }); + return; + } + + sendJson(res, 404, { error: "Not found" }); +} + +server.listen(port, bindHost, () => { + console.log(`Payabli local token server listening on http://${bindHost}:${port}`); + console.log(`Access token endpoint: http://${bindHost}:${port}/payabli/access-token`); +}); + +// The iOS app calls this endpoint to get a short-lived Payabli access token +// for both PayabliTTP and PayabliPayInPaymentFlow. In a real integration, +// this exchange happens on your own backend, authenticated by your own +// session mechanism — never in the mobile app itself. +async function resolveAccessToken() { + const directToken = process.env.PAYABLI_ACCESS_TOKEN?.trim(); + if (directToken) { + return directToken; + } + + if (cachedToken && cachedToken.expiresAt > Date.now()) { + return cachedToken.token; + } + + const clientId = process.env.CLIENT_ID?.trim(); + const clientSecret = process.env.CLIENT_SECRET?.trim(); + if (!clientId || !clientSecret) { + throw new Error("Set PAYABLI_ACCESS_TOKEN, or CLIENT_ID and CLIENT_SECRET, in .env."); + } + + const apiBaseUrl = normalizeBaseUrl(process.env.PAYABLI_API_BASE_URL || "https://api-sandbox.payabli.com/api"); + if (!allowedApiHosts.has(apiBaseUrl.hostname.toLowerCase())) { + throw new Error(`PAYABLI_API_BASE_URL host is not allowed: ${apiBaseUrl.hostname}`); + } + + const endpoint = new URL("v2/token/serverside", apiBaseUrl); + const upstream = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientId, clientSecret }) + }); + + const text = await upstream.text(); + const payload = text ? JSON.parse(text) : {}; + if (!upstream.ok) { + throw new Error(`Token exchange failed with HTTP ${upstream.status}: ${redact(JSON.stringify(payload))}`); + } + + const token = payload.access_token || payload.accessToken; + if (!token) { + throw new Error("Token exchange response did not include an access token."); + } + + cachedToken = { token, expiresAt: Date.now() + 5 * 60 * 1000 }; + return token; +} + +function sendJson(res, status, body) { + res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }); + res.end(JSON.stringify(body)); +} + +function normalizeBaseUrl(raw) { + const normalized = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`; + return new URL(normalized.endsWith("/") ? normalized : `${normalized}/`); +} + +function loadEnv(path) { + if (!existsSync(path)) return; + for (const rawLine of readFileSync(path, "utf8").split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + const separatorIndex = line.indexOf("="); + if (separatorIndex === -1) continue; + const key = line.slice(0, separatorIndex).trim(); + let value = line.slice(separatorIndex + 1).trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + if (key && process.env[key] === undefined) { + process.env[key] = value; + } + } +} + +function redact(text) { + return String(text).replace( + /("(?:access_token|accessToken|clientSecret|client_secret)"\s*:\s*)"[^"]*"/gi, + '$1"[REDACTED]"' + ); +} diff --git a/mobile/ios/project.yml b/mobile/ios/project.yml new file mode 100644 index 000000000..11e9122e5 --- /dev/null +++ b/mobile/ios/project.yml @@ -0,0 +1,54 @@ +name: PayabliPayInDemo +options: + createIntermediateGroups: true + deploymentTarget: + iOS: "16.7" + +packages: + PayabliSDK: + url: https://github.com/payabli/sdk-ios.git + branch: main + +targets: + PayabliPayInDemo: + type: application + platform: iOS + sources: + - path: PayabliPayInDemo + excludes: + - "Secrets.swift.sample" + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.payabli.examples.payinDemo + SWIFT_VERSION: "5.9" + CODE_SIGN_ENTITLEMENTS: PayabliPayInDemo/PayabliPayInDemo.entitlements + TARGETED_DEVICE_FAMILY: "1" + ENABLE_PREVIEWS: true + # sdk-ios ships PayabliSDKTapToPay and PayabliSDKPayInPaymentFlow as + # separate dynamic library products that both depend on the + # PayabliSDKCore target. Xcode's package graph can't reconcile that + # diamond when a single app target links both products (it works + # fine linking either one alone) and hard-fails the build without + # this flag. See mobile/ios/README.md#known-issue. + DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC: YES + info: + path: PayabliPayInDemo/Info.plist + properties: + UILaunchScreen: {} + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + NSAppTransportSecurity: + NSAllowsLocalNetworking: true + dependencies: + - package: PayabliSDK + product: PayabliSDKTapToPay + - package: PayabliSDK + product: PayabliSDKPayInPaymentFlow + +schemes: + PayabliPayInDemo: + build: + targets: + PayabliPayInDemo: all + run: + config: Debug diff --git a/mobile/ios/run.sh b/mobile/ios/run.sh new file mode 100755 index 000000000..df3654a06 --- /dev/null +++ b/mobile/ios/run.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Starts the local token server and builds + launches the example app in the +# iOS Simulator. Tap to Pay itself still needs a physical iPhone XS+ (see +# README.md) — this gets you the Pay In tab running in one command. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +SCHEME="PayabliPayInDemo" +BUNDLE_ID="com.payabli.examples.payinDemo" +DERIVED_DATA="$SCRIPT_DIR/.build" +APP_PATH="$DERIVED_DATA/Build/Products/Debug-iphonesimulator/$SCHEME.app" + +# --- First run: create config files and stop ----------------------------- + +NEEDS_SETUP=0 +if [[ ! -f TokenServer/.env ]]; then + cp TokenServer/.env.template TokenServer/.env + echo "Created TokenServer/.env — fill in CLIENT_ID/CLIENT_SECRET (or PAYABLI_ACCESS_TOKEN)." + NEEDS_SETUP=1 +fi +if [[ ! -f PayabliPayInDemo/Secrets.swift ]]; then + cp PayabliPayInDemo/Secrets.swift.sample PayabliPayInDemo/Secrets.swift + echo "Created PayabliPayInDemo/Secrets.swift — fill in entryPoint (and appId for Tap to Pay)." + NEEDS_SETUP=1 +fi +if [[ "$NEEDS_SETUP" == "1" ]]; then + echo "Fill those in, then run ./run.sh again." + exit 1 +fi + +# --- Token server ----------------------------------------------------- + +echo "Starting local token server..." +(cd TokenServer && exec node server.mjs) & +TOKEN_SERVER_PID=$! +trap 'echo "Stopping token server..."; kill "$TOKEN_SERVER_PID" 2>/dev/null || true' EXIT + +for _ in $(seq 1 20); do + curl -sf http://127.0.0.1:8787/health >/dev/null 2>&1 && break + sleep 0.5 +done +if ! curl -sf http://127.0.0.1:8787/health >/dev/null 2>&1; then + echo "Token server didn't come up — check TokenServer/.env." >&2 + exit 1 +fi +echo "Token server is up." + +# --- Xcode project -------------------------------------------------------- + +if command -v xcodegen >/dev/null 2>&1; then + echo "Regenerating Xcode project..." + xcodegen generate >/dev/null +else + echo "xcodegen not found (brew install xcodegen) — using the committed .xcodeproj as-is." +fi + +# --- Simulator ------------------------------------------------------------ + +SIM_UDID=$(xcrun simctl list devices | grep -E "iPhone.*\(Booted\)" | head -1 \ + | grep -oE '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}' || true) +if [[ -z "$SIM_UDID" ]]; then + SIM_UDID=$(xcrun simctl list devices available | grep -E "iPhone" | head -1 \ + | grep -oE '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}') +fi +if [[ -z "$SIM_UDID" ]]; then + echo "No iPhone simulator found. Create one in Xcode > Settings > Platforms." >&2 + exit 1 +fi + +echo "Booting simulator $SIM_UDID (if needed)..." +xcrun simctl bootstatus "$SIM_UDID" -b >/dev/null +open -a Simulator --args -CurrentDeviceUDID "$SIM_UDID" + +# --- Build, install, launch ------------------------------------------------ + +echo "Building $SCHEME (the first build can take a few minutes)..." +xcodebuild build \ + -project "$SCHEME.xcodeproj" \ + -scheme "$SCHEME" \ + -destination "platform=iOS Simulator,id=$SIM_UDID" \ + -derivedDataPath "$DERIVED_DATA" \ + CODE_SIGNING_ALLOWED=NO + +echo "Installing and launching..." +xcrun simctl install "$SIM_UDID" "$APP_PATH" +xcrun simctl launch "$SIM_UDID" "$BUNDLE_ID" + +cat <