Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.facebook.react.common.LifecycleState
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.frontegg.android.AdminPortalActivity
import com.frontegg.android.fronteggAuth
import com.frontegg.android.models.Entitlement
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import kotlin.time.DurationUnit
Expand Down Expand Up @@ -229,6 +230,29 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) :
promise.resolve(null)
}

@ReactMethod
fun loadEntitlements(forceRefresh: Boolean, promise: Promise) {
auth.loadEntitlements(forceRefresh) { success ->
promise.resolve(success)
}
}

@ReactMethod
fun getFeatureEntitlement(key: String, promise: Promise) {
promise.resolve(entitlementToMap(auth.getFeatureEntitlements(key)))
}

@ReactMethod
fun getPermissionEntitlement(key: String, promise: Promise) {
promise.resolve(entitlementToMap(auth.getPermissionEntitlements(key)))
}

private fun entitlementToMap(entitlement: Entitlement): WritableMap =
Arguments.createMap().apply {
putBoolean("isEntitled", entitlement.isEntitled)
putString("justification", entitlement.justification)
}

override fun getConstants(): MutableMap<String, Any?> = fronteggConstants.toMap()

companion object {
Expand Down
29 changes: 29 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,35 @@ export function MyScreen() {
}
```

## Entitlements

Gate features and permissions on-device. Call `loadEntitlements()` once the user is authenticated, then read feature/permission entitlements from the cache:

```tsx
import {
loadEntitlements,
getFeatureEntitlement,
getPermissionEntitlement,
} from '@frontegg/react-native';

// Load (or refresh) the current user's entitlements into the SDK cache.
await loadEntitlements();

// Check a feature-flag entitlement.
const feature = await getFeatureEntitlement('my-feature-key');
if (feature.isEntitled) {
// show the gated feature
} else {
console.log('Not entitled:', feature.justification);
}

// Check a permission entitlement.
const permission = await getPermissionEntitlement('fe.secure.read.users');
console.log(permission.isEntitled, permission.justification);
```

Each call resolves to an `Entitlement` — `{ isEntitled: boolean; justification?: string | null }`. When `isEntitled` is `false`, `justification` explains why (e.g. `"NOT_AUTHENTICATED"`, `"ENTITLEMENTS_NOT_LOADED"`, `"MISSING_FEATURE"`, `"MISSING_PERMISSION"`).

## Check user authentication state

Use the `useAuth` hook with `showLoader` and `isLoading` to conditionally display your app:
Expand Down
15 changes: 15 additions & 0 deletions ios/FronteggRN.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,19 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter)
openAdminPortal: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
loadEntitlements: (BOOL)forceRefresh
resolver: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
getFeatureEntitlement: (NSString *)key
resolver: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
getPermissionEntitlement: (NSString *)key
resolver: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
@end
36 changes: 36 additions & 0 deletions ios/FronteggRN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,42 @@ class FronteggRN: RCTEventEmitter {
}
}

@objc
func loadEntitlements(
_ forceRefresh: Bool,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
fronteggApp.auth.loadEntitlements(forceRefresh: forceRefresh) { success in
resolver(success)
}
}

@objc
func getFeatureEntitlement(
_ key: String,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
resolver(Self.entitlementToDict(fronteggApp.auth.getFeatureEntitlements(featureKey: key)))
}

@objc
func getPermissionEntitlement(
_ key: String,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
resolver(Self.entitlementToDict(fronteggApp.auth.getPermissionEntitlements(permissionKey: key)))
}

private static func entitlementToDict(_ entitlement: Entitlement) -> [String: Any] {
return [
"isEntitled": entitlement.isEntitled,
"justification": entitlement.justification ?? NSNull(),
]
}

private static func topViewController() -> UIViewController? {
let keyWindow = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
Expand Down
33 changes: 33 additions & 0 deletions src/FronteggNative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
}

export async function requestAuthorize(
refreshToken: string,

Check warning on line 65 in src/FronteggNative.ts

View workflow job for this annotation

GitHub Actions / Lint | Typecheck

'refreshToken' is already declared in the upper scope on line 42 column 23

Check warning on line 65 in src/FronteggNative.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck

'refreshToken' is already declared in the upper scope on line 42 column 23
deviceTokenCookie?: string
) {
return await FronteggRN.requestAuthorize(refreshToken, deviceTokenCookie);
Expand All @@ -89,6 +89,39 @@
return FronteggRN.openAdminPortal();
}

export interface Entitlement {
/** Whether the user is entitled to the requested feature/permission. */
isEntitled: boolean;
/**
* Optional reason when `isEntitled` is false — e.g. `"NOT_AUTHENTICATED"`,
* `"ENTITLEMENTS_NOT_LOADED"`, `"MISSING_FEATURE"`, `"MISSING_PERMISSION"`.
*/
justification?: string | null;
}

/**
* Loads the current user's entitlements into the SDK cache. Call this after
* authentication (and again with `forceRefresh` to refetch) before reading
* feature/permission entitlements. Resolves to `true` when entitlements loaded.
*/
export async function loadEntitlements(
forceRefresh: boolean = false
): Promise<boolean> {
return FronteggRN.loadEntitlements(forceRefresh);
}

/** Returns the on-device entitlement for a feature-flag key. */
export async function getFeatureEntitlement(key: string): Promise<Entitlement> {
return FronteggRN.getFeatureEntitlement(key);
}

/** Returns the on-device entitlement for a permission key. */
export async function getPermissionEntitlement(
key: string
): Promise<Entitlement> {
return FronteggRN.getPermissionEntitlement(key);
}

function debounce<T extends (...args: any[]) => any>(func: T, waitFor: number) {
let timeout: any;

Expand Down
4 changes: 4 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ export {
isSteppedUp,
stepUp,
openAdminPortal,
loadEntitlements,
getFeatureEntitlement,
getPermissionEntitlement,
} from './FronteggNative';
export type { Entitlement } from './FronteggNative';
Loading