diff --git a/FronteggRN.podspec b/FronteggRN.podspec index 417bd66..5cc8c47 100644 --- a/FronteggRN.podspec +++ b/FronteggRN.podspec @@ -15,15 +15,16 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/frontegg/frontegg-react-native.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" + s.exclude_files = "ios/Package.swift" # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. + # FronteggSwift via SPM — linked in post_integrate by ios/frontegg_spm.rb (see docs/setup.md). + if respond_to?(:install_modules_dependencies, true) install_modules_dependencies(s) - s.dependency "FronteggSwift", "1.3.10" else s.dependency "React-Core" - s.dependency "FronteggSwift", "1.3.10" # Don't install the dependencies when we run `pod install` in the old architecture. if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index 449ae18..1c6b4e2 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -14,6 +14,8 @@ import com.frontegg.android.AdminPortalActivity import com.frontegg.android.fronteggAuth import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable +import kotlin.time.DurationUnit +import kotlin.time.toDuration class FronteggRNModule(val reactContext: ReactApplicationContext) : @@ -158,6 +160,31 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : } } + @ReactMethod + fun isSteppedUp(maxAgeSeconds: Double, promise: Promise) { + val maxAge = + if (maxAgeSeconds < 0) null else maxAgeSeconds.toDuration(DurationUnit.SECONDS) + promise.resolve(auth.isSteppedUp(maxAge)) + } + + @ReactMethod + fun stepUp(maxAgeSeconds: Double, promise: Promise) { + val activity = currentActivity + if (activity == null) { + promise.reject("NO_ACTIVITY", "Current activity is null") + return + } + val maxAge = + if (maxAgeSeconds < 0) null else maxAgeSeconds.toDuration(DurationUnit.SECONDS) + auth.stepUp(activity, maxAge) { error -> + if (error != null) { + promise.reject("STEP_UP_ERROR", error.message, error) + } else { + promise.resolve(null) + } + } + } + @ReactMethod fun requestAuthorize(refreshToken: String, deviceTokenCookie: String?, promise: Promise) { try { diff --git a/docs/setup.md b/docs/setup.md index 978bd7e..b178ba7 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -4,6 +4,85 @@ This section guides you through configuring the Frontegg React Native SDK for bo ## Setup iOS Project +### Integrate FronteggSwift via Swift Package Manager + +The iOS native SDK (`FronteggSwift`) is delivered through **Swift Package Manager (SPM)** only. The CocoaPods pod `FronteggSwift` is **not** used (previously pinned at `1.2.76`). React Native still uses CocoaPods for `FronteggRN` and other dependencies; `FronteggSwift` is linked into the `FronteggRN` pod target via SPM after `pod install`. + +**Requirements:** Xcode **15+**, iOS deployment target **14.0+**, CocoaPods. + +#### 1. Update `ios/Podfile` + +Inside your app target (the same block as `use_native_modules!`): + +```ruby +target 'YourApp' do + # Required so the FronteggRN pod can import the FronteggSwift SPM product. + use_frameworks! :linkage => :static + + config = use_native_modules! + # ... use_react_native! etc. +``` + +After `post_install` (do **not** put the SPM hook inside `post_install` — CocoaPods would overwrite the patched `Pods` project): + +```ruby + post_install do |installer| + react_native_post_install(installer, config[:reactNativePath], :mac_catalyst_enabled => false) + # ...your other post_install hooks... + end + + post_integrate do |installer| + require_relative '../node_modules/@frontegg/react-native/ios/frontegg_spm' + FronteggSPM.link_frontegg_rn_pod(installer) + end +end +``` + +Adjust the `require_relative` path if your `node_modules` layout differs (monorepo, etc.). + +#### 2. Install pods + +From the `ios` directory: + +```bash +cd ios +bundle exec pod install # recommended if you use a Gemfile +``` + +**React Native 0.72:** if `pod install` fails downloading `boost`, add the boost URL workaround at the top of your `Podfile` (see the [example `ios/Podfile`](https://github.com/frontegg/frontegg-react-native/blob/master/example/ios/Podfile)). + +#### 3. Build and run + +- Open **`YourApp.xcworkspace`** (not `.xcodeproj`). +- For local development on a **simulator**, prefer: + +```bash +cd .. +yarn start # Metro — one terminal +yarn ios --simulator "iPhone 17 Pro Max" +``` + +If a physical iPhone is connected, React Native may target the device and fail on code signing (`com.your.bundle`). Use `--simulator` or disconnect the device. + +The `frontegg_spm` helper pins **FronteggSwift 1.3.10** from `https://github.com/frontegg/frontegg-ios-swift.git`, links it to the **FronteggRN** CocoaPods target, and configures the app Xcode project so SPM resolves without duplicate symbol errors at link time. + +> **Embedded mode step-up:** `FronteggSwift` **1.3.10** routes `stepUp()` through the embedded `WKWebView` when `embeddedMode` is enabled (instead of `ASWebAuthenticationSession`), so MFA/step-up reuses the existing web session. Set `embeddedMode` in `Frontegg.plist` if you use the embedded login box. + +#### Step-up from JavaScript + +```ts +import { isSteppedUp, stepUp } from '@frontegg/react-native'; + +const maxAgeSeconds = 60; +if (await isSteppedUp(maxAgeSeconds)) { + // already stepped up +} else { + await stepUp(maxAgeSeconds); +} +``` + +See the [example `HomeScreen`](https://github.com/frontegg/frontegg-react-native/blob/master/example/src/HomeScreen.tsx) for a **Sensitive action** button matching the native iOS SDK demo. + ### Create Frontegg.plist 1. Add a new file named `Frontegg.plist` to your root project directory. diff --git a/example/Gemfile.lock b/example/Gemfile.lock index ec146ff..0e51744 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -49,6 +49,8 @@ GEM cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) + cocoapods-spm (0.1.20) + xcodeproj (>= 1.23.0) cocoapods-trunk (1.6.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) @@ -92,6 +94,7 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.12) + cocoapods-spm (~> 0.1.20) RUBY VERSION ruby 2.6.10p210 diff --git a/example/README.md b/example/README.md index 12470c3..e994026 100644 --- a/example/README.md +++ b/example/README.md @@ -1,74 +1,32 @@ -This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). +# Frontegg React Native — example app -# Getting Started +## Prerequisites ->**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. +- [React Native environment](https://reactnative.dev/docs/environment-setup) (Node, Xcode 15+, CocoaPods) +- Configure `ios/Frontegg.plist` with your Frontegg `baseUrl` and `clientId` (see [Setup Guide](../docs/setup.md)) -## Step 1: Start the Metro Server +## iOS (SPM + CocoaPods) -First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. - -To start Metro, run the following command from the _root_ of your React Native project: +From the **example** directory: ```bash -# using npm -npm start - -# OR using Yarn -yarn start +yarn install +cd ios && bundle exec pod install && cd .. +yarn start # terminal 1 +yarn ios --simulator "iPhone 17 Pro Max" # terminal 2 — use an available simulator name ``` -## Step 2: Start your Application +Open `ios/ReactNativeExample.xcworkspace` in Xcode if you build from the IDE. -Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: +> Use `--simulator` when a physical iPhone is connected, otherwise `yarn ios` may try to deploy to the device and fail on provisioning. -### For Android +## Android ```bash -# using npm -npm run android - -# OR using Yarn yarn android ``` -### For iOS - -```bash -# using npm -npm run ios - -# OR using Yarn -yarn ios -``` - -If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. - -This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. - -## Step 3: Modifying your App - -Now that you have successfully run the app, let's modify it. - -1. Open `App.tsx` in your text editor of choice and edit some lines. -2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes! - - For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes! - -## Congratulations! :tada: - -You've successfully run and modified your React Native App. :partying_face: - -### Now what? - -- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). -- If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started). - -# Troubleshooting - -If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. - -# Learn More +## More To learn more about React Native, take a look at the following resources: diff --git a/example/ios/Podfile b/example/ios/Podfile index 5948679..f49d132 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,3 +1,18 @@ +# boostorg.jfrog.io often returns a bad tarball; use archives.boost.io (RN 0.72 workaround). +boost_podspec_path = File.expand_path('../node_modules/react-native/third-party-podspecs/boost.podspec', __dir__) +if File.exist?(boost_podspec_path) + boost_podspec = File.read(boost_podspec_path) + unless boost_podspec.include?('archives.boost.io') + File.write( + boost_podspec_path, + boost_podspec.gsub( + 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', + 'https://archives.boost.io/release/1.76.0/source/boost_1_76_0.tar.bz2' + ) + ) + end +end + # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', 'require.resolve( @@ -26,6 +41,9 @@ if linkage != nil end target 'ReactNativeExample' do + # Required so FronteggRN (Swift) can import the FronteggSwift SPM product. + use_frameworks! :linkage => :static + config = use_native_modules! # Flags change depending on the env values. @@ -59,4 +77,9 @@ target 'ReactNativeExample' do ) __apply_Xcode_12_5_M1_post_install_workaround(installer) end + + post_integrate do |installer| + require_relative '../../ios/frontegg_spm' + FronteggSPM.link_frontegg_rn_pod(installer) + end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6350afa..49ed929 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -10,12 +10,9 @@ PODS: - React-jsi (= 0.72.1) - ReactCommon/turbomodule/core (= 0.72.1) - fmt (6.2.1) - - FronteggRN (1.2.14): - - FronteggSwift (= 1.2.76) + - FronteggRN (1.2.16): - RCT-Folly (= 2021.07.22.00) - React-Core - - FronteggSwift (1.2.76): - - Sentry (~> 8.46.0) - glog (0.3.5) - hermes-engine (0.72.1): - hermes-engine/Pre-built (= 0.72.1) @@ -436,9 +433,6 @@ PODS: - RCT-Folly (= 2021.07.22.00) - React-Core - React-RCTImage - - Sentry (8.46.0): - - Sentry/Core (= 8.46.0) - - Sentry/Core (8.46.0) - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -491,9 +485,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - fmt - - FronteggSwift - libevent - - Sentry - SocketRocket EXTERNAL SOURCES: @@ -589,8 +581,7 @@ SPEC CHECKSUMS: FBLazyVector: 55cd4593d570bd9e5e227488d637ce6a9581ce51 FBReactNativeSpec: 799b0e1a1561699cd0e424e24fe5624da38402f0 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - FronteggRN: 38e704aa6bc3999f1251361e3bd17ff3ee4859b9 - FronteggSwift: df127087b79c5fc65240805136f36c7c28d4988d + FronteggRN: ce2450115a3c86ad7dbcd5b991fe1ff2bcab04a7 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 9df83855a0fd15ef8eb61694652bae636b0c466e libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 @@ -599,39 +590,38 @@ SPEC CHECKSUMS: RCTTypeSafety: 75fa444becadf0ebfa0a456b8c64560c7c89c7df React: 3e5b3962f27b7334eaf5517a35b3434503df35ad React-callinvoker: c3a225610efe0caadac78da53b6fe78e53eb2b03 - React-Codegen: fa4e79f8d56b5aa5f91d45984d21c21b31e211c8 - React-Core: cbfc97c3c0061d4fdaca617357086ba1347c730e - React-CoreModules: 0d18899272223a2a038e4c2313a7760ca6b280a7 - React-cxxreact: f78070b70980e9e19fb448fec9d49a9181952535 - React-debug: 8aa2bd54b0f0011049300ce3339b0e51254ef3b5 - React-hermes: c783d9dcf275ec48f97b164824dcdfdda480e6a6 - React-jsi: cc0d4c585001fca0979591587e00e6d2a4c13820 - React-jsiexecutor: a2b883b0e3fbc606a1e6e97a15059b71d6da1715 + React-Codegen: 319a74f758104130092a5984431045056c32fbab + React-Core: 40003a89fd94da98b03c3fad8834871299485cfa + React-CoreModules: 1304c0710deb87054623ccc9c552e23a1fcce219 + React-cxxreact: f82f0f1832606fabb9e8c9d61c4230704a3d2d2f + React-debug: 3e34ac6fb438f0e8c05a1678eedd6ebd8928245c + React-hermes: f076cb5f7351d6cc1600125bef3259ea880460fb + React-jsi: 9f381c8594161b2328b93cd3ba5d0bcfcd1e093a + React-jsiexecutor: 184eae1ecdedc7a083194bd9ff809c93f08fd34c React-jsinspector: d0b5bfd1085599265f4212034321e829bdf83cc0 - React-logger: 70abe174614ac3f165e02ab8300fec50d2f9572d - react-native-safe-area-context: 8745463257fac6150abd2281b02de640b3595ec7 - React-NativeModulesApple: 3f444b421d3c1e463414cb5aee385c4ffe6bae42 + React-logger: b8103c9b04e707b50cdd2b1aeb382483900cbb37 + react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2 + React-NativeModulesApple: bdfd86e972ea8103941faab9c7f64ea9be7e2be6 React-perflogger: 3d501f34c8d4b10cb75f348e43591765788525ad React-RCTActionSheet: f5335572c979198c0c3daff67b07bd1ad8370c1d - React-RCTAnimation: 5d0d31a4f9c49a70f93f32e4da098fb49b5ae0b3 - React-RCTAppDelegate: d285573b1d55da98149560f38df549293e4eb20d - React-RCTBlob: f7777ca0bad9e6d98efe921003f9f3cca70730f1 - React-RCTImage: e15d22db53406401cdd1407ce51080a66a9c7ed4 - React-RCTLinking: 39815800ec79d6fb15e6329244d195ebeabf7541 - React-RCTNetwork: 2a6548e13d2577b112d4250ac5be74ae62e1e86b - React-RCTSettings: a76aee44d5398144646be146c334b15c90ad9582 + React-RCTAnimation: 897b1170a1e934dee9b7eb7d5582920dea72c1c8 + React-RCTAppDelegate: f8dc4f198bb7a7fdc9ccd5a6e246936bbdacbe42 + React-RCTBlob: b69213136de8eed26fdf112602e8aaf91923093c + React-RCTImage: 9f2303a18cec1f715a447dca5b82021643661e6b + React-RCTLinking: f88453408f33a99f36bb4f6e11278ffbb81aa45f + React-RCTNetwork: 5ef8d399ad389442404683d2572007ace0e26076 + React-RCTSettings: 516d917b059c94f33f85f601974429de7c3d34ca React-RCTText: afad390f3838f210c2bc9e1a19bb048003b2a771 - React-RCTVibration: 29bbaa5c57c02dc036d7e557584b492000b1d3e7 + React-RCTVibration: 28cc9ac2c062b515f207a1743f0f27083d62acc5 React-rncore: 50966ce412d63bee9ffe5c98249857c23870a3c4 React-runtimeexecutor: d129f2b53d61298093d7c7d8ebfafa664f911ce6 - React-runtimescheduler: c8f959651fd874b93feeb32781b1e402a6801ec2 - React-utils: c9e15d684b491d6324b4139790306836f0e8b1d6 - ReactCommon: f7d2978a6f8e8552a4908f067e65d4cfda686138 - RNScreens: 67dd26e0c1e6df63f4ccaaf281c1ecdd88c995c3 - Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 + React-runtimescheduler: 34e27d6db52587cf93e7d9d44d5ff2dc4948243a + React-utils: 4324c58d17bcbcb8c49fe7f8c14b4f61d5dd4a42 + ReactCommon: e33f60179aae29423fcf4ea19fdedc038813ea3f + RNScreens: 835807a1f3c74c8ea0d0b9e089bf14333141f17f SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 65286bb6a07edce5e4fe8c90774da977ae8fc009 -PODFILE CHECKSUM: 010992354d93ba29cfe4cff5fab47f9b2920a2da +PODFILE CHECKSUM: fc09f9972b0d9b6a48315e1f02b457d29013ffd5 -COCOAPODS: 1.16.2 +COCOAPODS: 1.14.3 diff --git a/example/ios/ReactNativeExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeExample.xcodeproj/project.pbxproj index 6b25ee4..d7503f6 100644 --- a/example/ios/ReactNativeExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeExample.xcodeproj/project.pbxproj @@ -14,8 +14,8 @@ 41872B5B2AFBC5CF0014FB13 /* FronteggSwiftAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41872B5A2AFBC5CF0014FB13 /* FronteggSwiftAdapter.swift */; }; 41E63BA62A52DA4E00BF6DE4 /* Frontegg.plist in Resources */ = {isa = PBXBuildFile; fileRef = 41E63BA52A52DA4E00BF6DE4 /* Frontegg.plist */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - 9FAF02B86F1794E4CE2B25FC /* libPods-ReactNativeExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 48D7E04979345AE7E4960E7D /* libPods-ReactNativeExample.a */; }; - B24DB32AEF180101559169F0 /* libPods-ReactNativeExample-ReactNativeExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A2FEEAB4BC4E6A87D996DBDB /* libPods-ReactNativeExample-ReactNativeExampleTests.a */; }; + AC4FB686196FFEB9960B11F2 /* Pods_ReactNativeExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D49B967030CC3BCC1327ACBF /* Pods_ReactNativeExample.framework */; }; + B015078B67589DC0CDE8619B /* Pods_ReactNativeExample_ReactNativeExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2823F7D5F43B923F80107C24 /* Pods_ReactNativeExample_ReactNativeExampleTests.framework */; }; D847CC182D99D1DA00CAB625 /* FronteggLoaderInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D847CC172D99D1D900CAB625 /* FronteggLoaderInitializer.swift */; }; /* End PBXBuildFile section */ @@ -39,16 +39,16 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ReactNativeExample/main.m; sourceTree = ""; }; + 2823F7D5F43B923F80107C24 /* Pods_ReactNativeExample_ReactNativeExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactNativeExample_ReactNativeExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41872B5A2AFBC5CF0014FB13 /* FronteggSwiftAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FronteggSwiftAdapter.swift; sourceTree = ""; }; 41AE42B42B34A53900CB704F /* hermes.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermes.xcframework; path = "Pods/hermes-engine/destroot/Library/Frameworks/universal/hermes.xcframework"; sourceTree = ""; }; 41E63BA52A52DA4E00BF6DE4 /* Frontegg.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Frontegg.plist; sourceTree = ""; }; 41E63BA72A5430AB00BF6DE4 /* ReactNativeExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ReactNativeExample.entitlements; path = ReactNativeExample/ReactNativeExample.entitlements; sourceTree = ""; }; - 48D7E04979345AE7E4960E7D /* libPods-ReactNativeExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 62920412DB1DD6B64B10C127 /* Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig"; sourceTree = ""; }; 759B7129AA820E19A4DCA83A /* Pods-ReactNativeExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample.release.xcconfig"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeExample/LaunchScreen.storyboard; sourceTree = ""; }; - A2FEEAB4BC4E6A87D996DBDB /* libPods-ReactNativeExample-ReactNativeExampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeExample-ReactNativeExampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; CD982C31CEF36640C39F4113 /* Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig"; sourceTree = ""; }; + D49B967030CC3BCC1327ACBF /* Pods_ReactNativeExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactNativeExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D847CC172D99D1D900CAB625 /* FronteggLoaderInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FronteggLoaderInitializer.swift; sourceTree = ""; }; DBD4E57CAF9F6DF06DB159C4 /* Pods-ReactNativeExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -59,7 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B24DB32AEF180101559169F0 /* libPods-ReactNativeExample-ReactNativeExampleTests.a in Frameworks */, + B015078B67589DC0CDE8619B /* Pods_ReactNativeExample_ReactNativeExampleTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -67,7 +67,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9FAF02B86F1794E4CE2B25FC /* libPods-ReactNativeExample.a in Frameworks */, + AC4FB686196FFEB9960B11F2 /* Pods_ReactNativeExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -113,8 +113,8 @@ children = ( 41AE42B42B34A53900CB704F /* hermes.xcframework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 48D7E04979345AE7E4960E7D /* libPods-ReactNativeExample.a */, - A2FEEAB4BC4E6A87D996DBDB /* libPods-ReactNativeExample-ReactNativeExampleTests.a */, + D49B967030CC3BCC1327ACBF /* Pods_ReactNativeExample.framework */, + 2823F7D5F43B923F80107C24 /* Pods_ReactNativeExample_ReactNativeExampleTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -234,7 +234,10 @@ Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; - productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + packageReferences = ( + 2F0A5C48A15A74750BE517A0 /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */, + ); + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( @@ -626,6 +629,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -693,6 +704,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -749,6 +768,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 2F0A5C48A15A74750BE517A0 /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/frontegg/frontegg-ios-swift.git"; + requirement = { + kind = exactVersion; + version = 1.3.10; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6DD78663B3B3114CC0D8A6DA /* FronteggSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 2F0A5C48A15A74750BE517A0 /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */; + productName = FronteggSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..12df1f6 --- /dev/null +++ b/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "4101dbe3b3b6896fb67e506ae29b38a75a9292e5037ce292dd32c0e5083cf1d3", + "pins" : [ + { + "identity" : "frontegg-ios-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/frontegg/frontegg-ios-swift.git", + "state" : { + "revision" : "e702dd7d4fedd1290cee9ca9a72b0b8ed24a2fd3", + "version" : "1.3.10" + } + }, + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa.git", + "state" : { + "revision" : "dad229c665bfd043c5d80ac7aa77717cbd19a1c3", + "version" : "8.58.3" + } + } + ], + "version" : 3 +} diff --git a/example/src/HomeScreen.tsx b/example/src/HomeScreen.tsx index 71d8cbf..a52ac83 100644 --- a/example/src/HomeScreen.tsx +++ b/example/src/HomeScreen.tsx @@ -12,15 +12,49 @@ import { registerPasskeys, loginWithPasskeys, requestAuthorize, + isSteppedUp, + stepUp, openAdminPortal, } from '@frontegg/react-native'; import { useState } from 'react'; import type { ITenantsResponse } from '@frontegg/rest-api'; +const STEP_UP_MAX_AGE_SECONDS = 60; + export default function HomeScreen() { const [switching, setSwitching] = useState(''); + const [stepUpMessage, setStepUpMessage] = useState<{ + text: string; + isSuccess: boolean; + } | null>(null); const state = useAuth(); + const handleSensitiveAction = async () => { + setStepUpMessage(null); + try { + const alreadySteppedUp = await isSteppedUp(STEP_UP_MAX_AGE_SECONDS); + if (alreadySteppedUp) { + setStepUpMessage({ + text: 'You are already stepped up', + isSuccess: true, + }); + return; + } + await stepUp(STEP_UP_MAX_AGE_SECONDS); + setStepUpMessage({ + text: 'Action completed successfully', + isSuccess: true, + }); + } catch (error) { + setStepUpMessage({ + text: `Action completed with failure${ + error instanceof Error ? `: ${error.message}` : '' + }`, + isSuccess: false, + }); + } + }; + return ( @@ -62,23 +96,71 @@ export default function HomeScreen() { )} - - { - try { - const user = await requestAuthorize( - '1251386f-dcab-413b-aebf-c5b2903eccf6', - '0827d7b7-e07c-46c5-bb03-b85189d57d21' - ); - console.log('Authorization Success:', user); - } catch (error) { - console.error('Authorization Failed:', error); - } - }} - /> - + {state.isAuthenticated ? ( + + + + ) : null} + + {stepUpMessage ? ( + + + {stepUpMessage.text} + + + ) : null} + + {state.isAuthenticated ? ( + + { + if (!state.refreshToken) { + setStepUpMessage({ + text: 'No refresh token available', + isSuccess: false, + }); + return; + } + try { + const user = await requestAuthorize(state.refreshToken); + console.log('Authorization Success:', user); + setStepUpMessage({ + text: 'Request authorize successful', + isSuccess: true, + }); + } catch (error) { + console.error('Authorization Failed:', error); + setStepUpMessage({ + text: `Request authorize failed${ + error instanceof Error ? `: ${error.message}` : '' + }`, + isSuccess: false, + }); + } + }} + /> + + ) : null} {state.isAuthenticated ? ( @@ -215,4 +297,26 @@ const styles = StyleSheet.create({ marginBottom: 8, alignSelf: 'flex-start', }, + messageBanner: { + borderRadius: 16, + padding: 16, + marginVertical: 8, + width: '100%', + }, + messageBannerSuccess: { + backgroundColor: '#E8F5E9', + }, + messageBannerError: { + backgroundColor: '#FFEBEE', + }, + messageText: { + fontSize: 14, + fontWeight: '600', + }, + messageTextSuccess: { + color: '#2E7D32', + }, + messageTextError: { + color: '#C62828', + }, }); diff --git a/ios/FronteggRN.m b/ios/FronteggRN.m index 44aae96..1509d78 100644 --- a/ios/FronteggRN.m +++ b/ios/FronteggRN.m @@ -44,6 +44,16 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter) resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + isSteppedUp: (nonnull NSNumber *)maxAgeSeconds + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) +RCT_EXTERN_METHOD( + stepUp: (nonnull NSNumber *)maxAgeSeconds + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) RCT_EXTERN_METHOD( openAdminPortal: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject diff --git a/ios/FronteggRN.swift b/ios/FronteggRN.swift index 1228e15..7bc15cf 100644 --- a/ios/FronteggRN.swift +++ b/ios/FronteggRN.swift @@ -197,6 +197,41 @@ class FronteggRN: RCTEventEmitter { fronteggApp.auth.loginWithPasskeys(completion) } + /// React Native bridge requires nonnull NSNumber; pass -1 from JS when maxAge is omitted. + private func maxAgeInterval(from maxAgeSeconds: NSNumber) -> TimeInterval? { + let seconds = maxAgeSeconds.doubleValue + return seconds < 0 ? nil : seconds + } + + @objc + func isSteppedUp( + _ maxAgeSeconds: NSNumber, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + resolver(fronteggApp.auth.isSteppedUp(maxAge: maxAgeInterval(from: maxAgeSeconds))) + } + + @objc + func stepUp( + _ maxAgeSeconds: NSNumber, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + let completion: FronteggAuth.CompletionHandler = { result in + switch result { + case .success: + resolver(nil) + case .failure(let error): + rejecter(error.failureReason, error.localizedDescription, error) + } + } + + Task { + await fronteggApp.auth.stepUp(maxAge: maxAgeInterval(from: maxAgeSeconds), completion) + } + } + @objc func requestAuthorize( _ refreshToken: String, diff --git a/ios/Package.swift b/ios/Package.swift new file mode 100644 index 0000000..9feb10b --- /dev/null +++ b/ios/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 5.5 +// Reference manifest for the FronteggSwift version required by FronteggRN. +// React Native apps integrate this package via SPM in the host Podfile (see docs/setup.md). + +import PackageDescription + +let package = Package( + name: "FronteggRNPackage", + platforms: [ + .iOS(.v14) + ], + products: [], + dependencies: [ + .package(url: "https://github.com/frontegg/frontegg-ios-swift.git", exact: "1.3.10"), + ], + targets: [] +) diff --git a/ios/frontegg_spm.rb b/ios/frontegg_spm.rb new file mode 100644 index 0000000..2ecc065 --- /dev/null +++ b/ios/frontegg_spm.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +# Links FronteggSwift (SPM 1.3.10) to the FronteggRN CocoaPods target after `pod install`. +# Patches Pods.xcodeproj/project.pbxproj as text — xcodeproj gem save() breaks Xcode 26. +module FronteggSPM + PACKAGE_URL = 'https://github.com/frontegg/frontegg-ios-swift.git' + PACKAGE_VERSION = '1.3.10' + PRODUCT_NAME = 'FronteggSwift' + + PACKAGE_REF_ID = '2F0A5C48A15A74750BE517A0' + PRODUCT_DEP_ID = '6DD78663B3B3114CC0D8A6DA' + FRONTEGG_RN_TARGET_ID = 'A8A32B38A9DEF00EF3A61386B5FF1887' + PODS_PROJECT_ID = '46EB2E00000000' + FRONTEGG_RN_FRAMEWORKS_PHASE_ID = '46EB2E000076A0' + SPM_TARGET_DEP_ID = '7A1B2C3D4E5F6000100140001' + SPM_FRAMEWORK_BUILD_ID = '7A1B2C3D4E5F6000100140002' + + module_function + + def link_frontegg_rn_pod(installer) + pbxproj_path = File.join(installer.sandbox.root, 'Pods.xcodeproj', 'project.pbxproj') + patch_pods_pbxproj(pbxproj_path) + patch_frontegg_rn_xcconfigs(installer.sandbox.root) + patch_app_pods_xcconfigs(installer.sandbox.root) + + app_pbxproj = user_app_pbxproj_path(installer) + dedupe_app_frontegg_spm_link(app_pbxproj) if app_pbxproj + end + + def user_app_pbxproj_path(installer) + ios_dir = installer.sandbox.root.parent + Dir.glob(File.join(ios_dir, '*.xcodeproj', 'project.pbxproj')) + .reject { |path| path.include?('/Pods.xcodeproj/') } + .first + end + + def patch_app_pods_xcconfigs(pods_root) + spm_search_path = '"$(PODS_CONFIGURATION_BUILD_DIR)"' + Dir.glob(File.join(pods_root, 'Target Support Files', 'Pods-*', 'Pods-*.xcconfig')).each do |path| + content = File.read(path) + unless content.include?('SWIFT_INCLUDE_PATHS') + content = "#{content}\nSWIFT_INCLUDE_PATHS = $(inherited) #{spm_search_path}\n" + end + swift_import_flag = '-I"$(PODS_CONFIGURATION_BUILD_DIR)"' + if content.include?('OTHER_SWIFT_FLAGS =') + content = content.sub(/OTHER_SWIFT_FLAGS = (.+)/) do |line| + line.include?(swift_import_flag) ? line : line.sub(/\Z/, " #{swift_import_flag}") + end + else + content = "#{content}\nOTHER_SWIFT_FLAGS = $(inherited) #{swift_import_flag}\n" + end + File.write(path, content) + end + end + + # App Swift files import FronteggSwift but must not link it (FronteggRN already does). + # Keep package resolution on the app project; drop target-level package products only. + def dedupe_app_frontegg_spm_link(app_pbxproj_path) + return unless File.exist?(app_pbxproj_path) + + content = File.read(app_pbxproj_path) + content = content.sub(%r{^// FRONTEGG_SPM_DEDUPED_MARKER\n}, '') + + content = content.gsub( + /\n\t\t\tpackageProductDependencies = \(\n\t\t\t\t#{PRODUCT_DEP_ID} \/\* #{PRODUCT_NAME} \*\/,\n\t\t\t\);/, + '' + ) + + unless content.include?('XCRemoteSwiftPackageReference "frontegg-ios-swift.git"') + content = content.sub( + /(mainGroup = [A-F0-9]+;\n)(\t\t\tproductRefGroup)/, + "\\1\t\t\tpackageReferences = (\n\t\t\t\t#{PACKAGE_REF_ID} /* XCRemoteSwiftPackageReference \"frontegg-ios-swift.git\" */,\n\t\t\t);\n\t\t\t\\2" + ) + + package_sections = <<~PBX + + /* Begin XCRemoteSwiftPackageReference section */ + \t\t#{PACKAGE_REF_ID} /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */ = { + \t\t\tisa = XCRemoteSwiftPackageReference; + \t\t\trepositoryURL = "#{PACKAGE_URL}"; + \t\t\trequirement = { + \t\t\t\tkind = exactVersion; + \t\t\t\tversion = #{PACKAGE_VERSION}; + \t\t\t}; + \t\t}; + /* End XCRemoteSwiftPackageReference section */ + + /* Begin XCSwiftPackageProductDependency section */ + \t\t#{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */ = { + \t\t\tisa = XCSwiftPackageProductDependency; + \t\t\tpackage = #{PACKAGE_REF_ID} /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */; + \t\t\tproductName = #{PRODUCT_NAME}; + \t\t}; + /* End XCSwiftPackageProductDependency section */ + PBX + + content.sub!(/\n\t\};\n\trootObject = /, "#{package_sections}\n\t};\n\trootObject = ") + end + + File.write(app_pbxproj_path, content) + end + + def patch_frontegg_rn_xcconfigs(pods_root) + Dir.glob(File.join(pods_root, 'Target Support Files', 'FronteggRN', 'FronteggRN.*.xcconfig')).each do |path| + content = File.read(path) + # FronteggSwift.swiftmodule is emitted alongside FronteggRN under PODS_CONFIGURATION_BUILD_DIR. + spm_search_path = '"$(PODS_CONFIGURATION_BUILD_DIR)"' + lines = { + 'CLANG_ENABLE_MODULES' => 'YES', + 'SWIFT_ENABLE_EXPLICIT_MODULES' => 'NO', + 'SWIFT_INCLUDE_PATHS' => "$(inherited) #{spm_search_path}" + } + lines.each do |key, value| + if content.include?("#{key} =") + content = content.gsub(/#{Regexp.escape(key)} = .*/, "#{key} = #{value}") + else + content = "#{content}\n#{key} = #{value}\n" + end + end + unless content.match?(/FRAMEWORK_SEARCH_PATHS = .*\$\(PODS_CONFIGURATION_BUILD_DIR\)"/) + content = content.sub( + /(FRAMEWORK_SEARCH_PATHS = .+)/, + "\\1 #{spm_search_path}" + ) + end + swift_import_flag = '-I"$(PODS_CONFIGURATION_BUILD_DIR)"' + if content.include?('OTHER_SWIFT_FLAGS =') + content = content.sub(/OTHER_SWIFT_FLAGS = (.+)/) do |line| + line.include?(swift_import_flag) ? line : line.sub(/\Z/, " #{swift_import_flag}") + end + else + content = "#{content}\nOTHER_SWIFT_FLAGS = $(inherited) #{swift_import_flag}\n" + end + File.write(path, content) + end + end + + def patch_pods_pbxproj(pbxproj_path) + content = File.read(pbxproj_path) + + unless content.include?("#{FRONTEGG_RN_TARGET_ID} /* FronteggRN */") + raise "FronteggSPM: FronteggRN target not found in #{pbxproj_path}" + end + + unless content.include?('XCRemoteSwiftPackageReference "frontegg-ios-swift.git"') + content = add_package_reference(content) + else + content = ensure_package_product_dependencies(content) + end + + content = add_spm_target_dependency(content) + content = add_spm_framework_build_file(content) + File.write(pbxproj_path, content) + content + end + + def add_package_reference(content) + content = content.sub( + /(#{Regexp.escape(FRONTEGG_RN_TARGET_ID)} \/\* FronteggRN \*\/ = \{[^}]*?name = FronteggRN;\n\t\t\tproductName = FronteggRN;)/m + ) do |block| + if block.include?('packageProductDependencies') + block + else + block.sub( + "name = FronteggRN;\n\t\t\tproductName = FronteggRN;", + "name = FronteggRN;\n\t\t\tpackageProductDependencies = (\n\t\t\t\t#{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */,\n\t\t\t);\n\t\t\tproductName = FronteggRN;" + ) + end + end + + content = content.sub( + /(#{Regexp.escape(PODS_PROJECT_ID)} \/\* Project object \*\/ = \{[^}]*?mainGroup = 46EB2E00000010;\n)/m + ) do |header| + if header.include?('packageReferences') + header + else + "#{header}\t\t\tpackageReferences = (\n\t\t\t\t#{PACKAGE_REF_ID} /* XCRemoteSwiftPackageReference \"frontegg-ios-swift.git\" */,\n\t\t\t);\n" + end + end + + package_sections = <<~PBX + + /* Begin XCRemoteSwiftPackageReference section */ + \t\t#{PACKAGE_REF_ID} /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */ = { + \t\t\tisa = XCRemoteSwiftPackageReference; + \t\t\trepositoryURL = "#{PACKAGE_URL}"; + \t\t\trequirement = { + \t\t\t\tkind = exactVersion; + \t\t\t\tversion = #{PACKAGE_VERSION}; + \t\t\t}; + \t\t}; + /* End XCRemoteSwiftPackageReference section */ + + /* Begin XCSwiftPackageProductDependency section */ + \t\t#{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */ = { + \t\t\tisa = XCSwiftPackageProductDependency; + \t\t\tpackage = #{PACKAGE_REF_ID} /* XCRemoteSwiftPackageReference "frontegg-ios-swift.git" */; + \t\t\tproductName = #{PRODUCT_NAME}; + \t\t}; + /* End XCSwiftPackageProductDependency section */ + PBX + + content.sub(/\n\t\};\n\trootObject = /, "#{package_sections}\n\t};\n\trootObject = ") + end + + def ensure_package_product_dependencies(content) + return content if content.include?("#{FRONTEGG_RN_TARGET_ID} /* FronteggRN */") && + content.match?(/#{Regexp.escape(FRONTEGG_RN_TARGET_ID)} \/\* FronteggRN \*\/ = \{[\s\S]*?packageProductDependencies =/) + + content.sub( + /(#{Regexp.escape(FRONTEGG_RN_TARGET_ID)} \/\* FronteggRN \*\/ = \{[^}]*?name = FronteggRN;\n\t\t\tproductName = FronteggRN;)/m + ) do |block| + block.sub( + "name = FronteggRN;\n\t\t\tproductName = FronteggRN;", + "name = FronteggRN;\n\t\t\tpackageProductDependencies = (\n\t\t\t\t#{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */,\n\t\t\t);\n\t\t\tproductName = FronteggRN;" + ) + end + end + + def add_spm_target_dependency(content) + return content if content.include?("productRef = #{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */") + + content = content.sub( + /(#{Regexp.escape(FRONTEGG_RN_TARGET_ID)} \/\* FronteggRN \*\/ = \{[\s\S]*?dependencies = \(\n)/m + ) do |match| + "#{match}\t\t\t\t#{SPM_TARGET_DEP_ID} /* PBXTargetDependency */,\n" + end + + dep_entry = <<~DEP + \t\t#{SPM_TARGET_DEP_ID} /* PBXTargetDependency */ = { + \t\t\tisa = PBXTargetDependency; + \t\t\tproductRef = #{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */; + \t\t}; + DEP + + if content.include?('/* End PBXTargetDependency section */') + content.sub('/* End PBXTargetDependency section */', "#{dep_entry}/* End PBXTargetDependency section */") + else + content.sub(/\n\t\};\n\trootObject = /, "\n/* Begin PBXTargetDependency section */#{dep_entry}/* End PBXTargetDependency section */\n\t};\n\trootObject = ") + end + end + + def add_spm_framework_build_file(content) + return content if content.include?("#{SPM_FRAMEWORK_BUILD_ID} /* #{PRODUCT_NAME} in Frameworks */") + + build_file = "\t\t#{SPM_FRAMEWORK_BUILD_ID} /* #{PRODUCT_NAME} in Frameworks */ = {isa = PBXBuildFile; productRef = #{PRODUCT_DEP_ID} /* #{PRODUCT_NAME} */; };\n" + content = content.sub('/* Begin PBXBuildFile section */', "/* Begin PBXBuildFile section */\n#{build_file}") + + content.sub( + /(#{Regexp.escape(FRONTEGG_RN_FRAMEWORKS_PHASE_ID)} \/\* Frameworks \*\/ = \{[\s\S]*?files = \(\n)/m + ) do |match| + "#{match}\t\t\t\t#{SPM_FRAMEWORK_BUILD_ID} /* #{PRODUCT_NAME} in Frameworks */,\n" + end + end +end diff --git a/src/FronteggNative.ts b/src/FronteggNative.ts index d8115f4..9c1b2db 100644 --- a/src/FronteggNative.ts +++ b/src/FronteggNative.ts @@ -68,6 +68,19 @@ export async function requestAuthorize( return await FronteggRN.requestAuthorize(refreshToken, deviceTokenCookie); } +/** Sentinel for optional maxAge on the native bridge (NSNumber must be nonnull on iOS). */ +const NO_MAX_AGE = -1; + +/** Max age in seconds for step-up validity (same semantics as native SDK). */ +export async function isSteppedUp(maxAge?: number): Promise { + return FronteggRN.isSteppedUp(maxAge ?? NO_MAX_AGE); +} + +/** Starts step-up authentication (MFA / re-auth). Max age in seconds. */ +export async function stepUp(maxAge?: number): Promise { + return FronteggRN.stepUp(maxAge ?? NO_MAX_AGE); +} + export async function registerPasskeys(): Promise { return FronteggRN.registerPasskeys(); } diff --git a/src/index.tsx b/src/index.tsx index 17ee54c..d82856d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,5 +11,7 @@ export { loginWithPasskeys, registerPasskeys, requestAuthorize, + isSteppedUp, + stepUp, openAdminPortal, } from './FronteggNative';