# AppFig Documentation — AI-Ready Packet

Generated: 2026-07-15T20:24:40.896Z
Source: https://appfig.com/docs

This file concatenates every AppFig documentation page into one document, intended to be pasted or uploaded into an AI tool (Claude Project knowledge, a custom GPT, etc.) as context for answering support questions about AppFig.


---

<!-- SOURCE: AppFig-docs/Quick Start/Quick Start Guide.md -->

# AppFig Quick Start

Feature flags and remote config that evaluate rules client-side. Zero latency, maximum control.

## Install & Initialize

### Unity

```csharp
// Local mode (free)
AppFig.InitLocal();

// Cloud mode (paid)
AppFig.Init("company-id", "tenant-id", "prod", "api-key");
```

### React/Web

```typescript
import AppFig from './AppFigReactSDK';

await AppFig.init({
  companyId: 'company-id',
  tenantId: 'tenant-id',
  env: 'prod',
  apiKey: 'api-key'
});
```

### iOS

```swift
AppFig.initialize(
  companyId: "company-id",
  tenantId: "tenant-id",
  environment: "prod",
  apiKey: "api-key"
)
```

### Android

```kotlin
AppFig.init(
  context = this,
  companyId = "company-id",
  tenantId = "tenant-id",
  env = "prod",
  apiKey = "api-key"
)
```

## Log Events

### Unity
```csharp
AppFig.LogEvent("level_complete");
AppFig.LogEvent("purchase", new Dictionary<string, string> {
  { "item_id", "sword" },
  { "price", "9.99" }
});
```

### React/Web
```typescript
AppFig.logEvent('button_clicked');
AppFig.logEvent('purchase', { item_id: 'sword', price: '9.99' });
```

### iOS
```swift
AppFig.logEvent(name: "level_complete")
AppFig.logEvent(name: "purchase", parameters: ["item_id": "sword"])
```

### Android
```kotlin
AppFig.logEvent("level_complete")
AppFig.logEvent("purchase", mapOf("item_id" to "sword"))
```

## Check Features

### Unity
```csharp
if (AppFig.IsFeatureEnabled("double_xp")) {
  ApplyDoubleXP();
}

string maxLives = AppFig.GetFeatureValue("max_lives");
```

### React/Web
```typescript
const showBanner = AppFig.getConfig('premium_banner');
if (showBanner === 'on') {
  renderBanner();
}
```

### iOS
```swift
if AppFig.isFeatureEnabled("double_xp") {
  applyDoubleXP()
}

let maxLives = AppFig.getFeatureValue("max_lives")
```

### Android
```kotlin
if (AppFig.isFeatureEnabled("double_xp")) {
  applyDoubleXP()
}

val maxLives = AppFig.getFeatureValue("max_lives")
```

## Set Properties

### Unity
```csharp
AppFig.SetUserProperty("tier", "premium");
AppFig.SetUserProperty("level", "15");
```

### React/Web
```typescript
AppFig.setUserProperties({
  tier: 'premium',
  level: '15'
});
```

### iOS
```swift
AppFig.setUserProperty(key: "tier", value: "premium")
AppFig.setUserProperty(key: "level", value: "15")
```

### Android
```kotlin
AppFig.setUserProperty("tier", "premium")
AppFig.setUserProperty("level", "15")
```

## How It Works

1. **Define rules** in dashboard or JSON file
2. **SDK downloads** rules on launch (cached)
3. **App logs events** as users interact
4. **SDK evaluates** rules locally, instantly
5. **Features respond** to user behavior

## Example Rules

### Veteran Bonus
```
IF event "level_complete" count >= 10
THEN "veteran_bonus" = "2x"
```

### Platform-Specific
```
IF device.platform IN ["iOS", "Android"]
THEN "mobile_controls" = "on"
```

### Country Pricing
```
IF device.country == "US"
THEN "price" = "9.99"
```

### A/B Test
```
IF user.id % 2 == 0 THEN "button_color" = "red"
IF user.id % 2 == 1 THEN "button_color" = "blue"
```

## Local vs Cloud Mode

| Feature | Local (Free) | Cloud (Paid) |
|---------|--------------|--------------|
| Feature flags | ✅ Unlimited | ✅ Unlimited |
| Rule evaluation | ✅ Client-side | ✅ Client-side |
| Rules source | JSON file | Cloud sync |
| Updates | App rebuild | Auto-refresh |
| Dashboard | ❌ | ✅ |
| Team collab | ❌ | ✅ |
| Environments | ❌ | ✅ Dev/Prod |

## Supported Platforms

- **Unity** - C# SDK (iOS, Android, Windows, macOS, WebGL)
- **React/Web** - TypeScript SDK (all browsers)
- **iOS** - Swift SDK (iPhone, iPad)
- **Android** - Kotlin SDK (phones, tablets)

## Next Steps

**Full Guides:**
- [Android Integration](#AppFig-docs%2FSDK-Documentation%2Fandroid-integration.md)
- [iOS Integration](#AppFig-docs%2FSDK-Documentation%2Fios-integration.md)
- [React/Web Guide](#AppFig-docs%2FSDK-Documentation%2Freact-web-guide.md)
- [Unity Integration](#AppFig-docs%2FSDK-Documentation%2Funity-integration.md)

**Concepts:**
- [Core Concepts](#AppFig-docs%2FGetting%20Started%2Fconcepts.md)
- [How It Works](#AppFig-docs%2FGetting%20Started%2Fhow-it-works.md)

**Reference:**
- [Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)
- [A/B Testing](#AppFig-docs%2FReference%2Fab-testing.md)
- [Rule Prioritization](#AppFig-docs%2FReference%2Frule-prioritization.md)
- [Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)
- [Local Mode](#AppFig-docs%2FAdvanced%2Flocal-mode.md)

## Auto-Detected Properties

The SDK automatically detects and sets these properties:

```
platform       "iOS", "Android", "Windows", "macOS", etc.
os_version     "17.0", "13", "11.0"
language       "English", "Japanese", "Spanish"
timezone       "America/Los_Angeles", "Asia/Tokyo"
country        "US", "JP", "GB"
device_brand   "Apple", "Samsung", "Google"
device_model   "iPhone 15", "Galaxy S23", "Pixel 8"
```

Use these in rules without any setup required.

## Common Patterns

### Feature Rollout
```csharp
// Enable for users who completed onboarding
Rule: IF event "onboarding_complete" count >= 1
      THEN "new_feature" = "on"
```

### Time-Limited Event
```csharp
// Show holiday event based on timezone
Rule: IF device.timezone IN ["America/Los_Angeles", "America/New_York"]
      AND event "app_open" count >= 1
      THEN "holiday_event" = "on"
```

### Premium Features
```csharp
// Unlock features for premium users
Rule: IF user.subscription_tier == "premium"
      THEN "unlimited_lives" = "on"
```

### Progressive Unlock
```csharp
// Increase lives as player progresses
Rule 1: IF event "level_complete" count >= 5  THEN "max_lives" = "5"
Rule 2: IF event "level_complete" count >= 10 THEN "max_lives" = "7"
Rule 3: IF event "level_complete" count >= 20 THEN "max_lives" = "10"
```

## Best Practices

1. **Cache feature values** - Don't check every frame
2. **Handle null** - Provide fallback values
3. **Log liberally** - More events = better targeting
4. **Test rules** - Verify before deploying
5. **Use local mode** - Free development/testing

## Quick Troubleshooting

**Features return null?**
- Check feature name spelling (case-sensitive)
- Verify rules are loaded
- Log events/properties first

**Rules not updating?**
- Wait for poll interval (cloud mode)
- Call `refreshRules()` manually
- Check network connection

**SDK not initialized?**
- Must call init before any SDK methods
- Initialize in app entry point

---

**Ready to start?** Pick your platform above and follow the full integration guide.


---

<!-- SOURCE: AppFig-docs/Getting Started/concepts.md -->

# Core Concepts

Understanding these four concepts will help you master AppFig in minutes.

## 1. Features

**A feature is something you want to control remotely.**

Features have:
- A **name** (e.g., "double_xp", "max_lives", "welcome_message")
- A **value** (e.g., "on", "5", "Welcome back!")

```csharp
// Check if a feature is enabled (value == "on")
if (AppFig.IsFeatureEnabled("double_xp")) {
    ApplyDoubleXP();
}

// Get a feature's value
string maxLives = AppFig.GetFeatureValue("max_lives"); // Returns "5"
int lives = int.Parse(maxLives);
```

**Common feature types:**
- **Boolean flags**: "new_ui" = "on" or "off"
- **Numeric values**: "max_lives" = "5"
- **Text content**: "welcome_message" = "Hello!"
- **Complex data**: "pricing" = JSON object

## 2. Rules

**Rules determine when a feature should activate and what value it should have.**

A feature can have multiple rules. The **first matching rule** wins.

```json
Feature: "veteran_bonus"
  Rule 1: IF user completed 50+ levels → value = "3x"
  Rule 2: IF user completed 10+ levels → value = "2x"
  (No match) → feature returns null
```

**Rule structure:**
```json
{
  "conditions": {
    "events": [...],           // User actions
    "user_properties": [...],  // User attributes
    "device": [...]            // Device/platform info
  },
  "value": "on"                // What to return if conditions match
}
```

All three condition types must pass for a rule to match (AND logic between types).

## 3. Events

**Events are user actions your app logs.**

```csharp
// Simple event
AppFig.LogEvent("level_complete");

// Event with parameters
AppFig.LogEvent("purchase_made", new Dictionary<string, string> {
    { "item", "sword" },
    { "price", "4.99" }
});
```

**Rules can match based on:**
- **Count**: User completed 10+ levels
- **Recency**: User opened app in last 7 days
- **Parameters**: User purchased items priced over $10
- **Sequence**: User did A, then B, then C (in order)

**Example rule:**
```json
{
  "conditions": {
    "events": {
      "mode": "simple",
      "operator": "AND",
      "items": [
        {
          "key": "level_complete",
          "count": {
            "operator": ">=",
            "value": 10
          },
          "within_last_days": 7
        }
      ]
    }
  },
  "value": "on"
}
```

This matches if user completed 10+ levels in the last 7 days.

## 4. Properties

**Properties are attributes that describe users and devices.**

### User Properties

Set by your app to describe the user:

```csharp
AppFig.SetUserProperty("subscription_tier", "premium");
AppFig.SetUserProperty("account_age_days", "45");
AppFig.SetUserProperty("preferred_language", "en");
```

**Example rule:**
```json
{
  "conditions": {
    "user_properties": [
      {
        "key": "subscription_tier",
        "value": {
          "operator": "==",
          "value": "premium"
        }
      }
    ]
  },
  "value": "on"
}
```

### Device Properties

Auto-detected by the SDK (no manual setup):

```
platform: "iOS", "Android", "Windows", etc.
os_version: "15.0", "13", "11"
language: "en-US", "ja", "es"
country: "US", "JP", "GB"
device_brand: "Apple", "Samsung", "Google"
device_model: "iPhone 14", "Galaxy S23"
```

**Example rule:**
```json
{
  "conditions": {
    "device": [
      {
        "key": "platform",
        "value": {
          "operator": "in",
          "value": ["iOS", "Android"]
        }
      }
    ]
  },
  "value": "on"
}
```

This enables the feature only on mobile platforms.

## How They Work Together

```
1. Your app logs events

   AppFig.LogEvent("level_complete") // Called 12 times

2. AppFig evaluates all rules for a feature

   Feature: "veteran_bonus"

   Rule 1: Check conditions
     ✓ Events: level_complete count >= 10? YES (12 logged)
     ✓ User properties: None required
     ✓ Device: None required
     ✅ Rule 1 passes!

3. First matching rule returns its value

   AppFig.GetFeatureValue("veteran_bonus") // Returns "2x"
```

See [How It Works](#AppFig-docs%2FGetting%20Started%2Fhow-it-works.md) for the full step-by-step evaluation trace of this exact example.

## Key Principles

### 1. Client-Side Evaluation
Rules are evaluated **on your device**, not on a server. This means:
- ✅ Zero latency (no network calls)
- ✅ Works offline
- ✅ No sensitive data sent to servers

### 2. First Match Wins
When multiple rules could match, the **first one wins**. Order matters!

```json
Feature: "discount"
  Rule 1: IF premium user → "50% off"
  Rule 2: IF any user → "10% off"

Premium users will get 50% (Rule 1 matches first)
Free users will get 10% (Rule 1 doesn't match, Rule 2 does)
```

### 3. Null vs Empty String
If no rules match, `GetFeatureValue()` returns `null` (not an empty string).

```csharp
string value = AppFig.GetFeatureValue("veteran_bonus");
if (value == null) {
    // Feature has no matching rules
}
```

### 4. All Conditions Must Pass
Within a rule, ALL condition types must pass:
- Events AND
- User Properties AND
- Device Properties

But within each type, you can use AND or OR operators.

## Next Steps

- **[Quick Start Guide](#AppFig-docs%2FQuick%20Start%2FQuick%20Start%20Guide.md)** - Set up your first feature
- **[How It Works](#AppFig-docs%2FGetting%20Started%2Fhow-it-works.md)** - Deep dive into rule evaluation
- **[Rule Schema Reference](#AppFig-docs%2FReference%2Frule-schema.md)** - All operators and options

## Examples

### Example 1: Welcome Bonus for New Users
```csharp
// Rule: Show welcome bonus to users who opened app < 3 times
AppFig.LogEvent("app_open");
if (AppFig.IsFeatureEnabled("welcome_bonus")) {
    ShowWelcomeBonus();
}
```

### Example 2: Platform-Specific UI
```csharp
// Rule: Show mobile controls only on mobile
// Device property "platform" is auto-detected
if (AppFig.IsFeatureEnabled("mobile_controls")) {
    EnableTouchControls();
}
```

### Example 3: Subscription-Based Content
```csharp
// Set user property once after checking subscription
AppFig.SetUserProperty("subscription_tier", "premium");

// Rule: Show premium content only to premium users
if (AppFig.IsFeatureEnabled("premium_content")) {
    ShowPremiumLevels();
}
```

### Example 4: Engagement-Based Unlocks
```csharp
// Log events as user plays
AppFig.LogEvent("level_complete");
AppFig.LogEvent("level_complete");
// ... 10 times total

// Rule: Unlock hard mode after 10 level completions
if (AppFig.IsFeatureEnabled("hard_mode")) {
    UnlockHardMode();
}
```


---

<!-- SOURCE: AppFig-docs/Getting Started/how-it-works.md -->

# How It Works

A detailed look at how AppFig evaluates rules on the client side.

## The Big Picture

```
┌─────────────────────────────────────────────────────────────┐
│ 1. INITIALIZATION                                           │
│    Your app starts → SDK loads rules (from file or server) │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. TRACKING                                                 │
│    Your app logs events, sets properties                    │
│    - AppFig.LogEvent("level_complete")                      │
│    - AppFig.SetUserProperty("tier", "premium")              │
│    All stored locally in memory                             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. EVALUATION (when you call GetFeatureValue)              │
│    SDK checks rules against stored events/properties        │
│    - Loops through rules in order                           │
│    - First matching rule wins                               │
│    - Returns value or null                                  │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. YOUR APP USES THE VALUE                                 │
│    if (value == "on") { EnableFeature(); }                  │
└─────────────────────────────────────────────────────────────┘
```

## Step-by-Step: Rule Evaluation

Let's walk through exactly what happens when you call `GetFeatureValue()`.

### Setup

You have this feature defined:

```json
Feature: "veteran_bonus"
Rules: [
  {
    "conditions": {
      "events": {
        "mode": "simple",
        "operator": "AND",
        "items": [
          {
            "key": "level_complete",
            "count": { "operator": ">=", "value": 10 }
          }
        ]
      },
      "user_properties": [],
      "device": []
    },
    "value": "2x"
  }
]
```

Your app has done this:

```csharp
AppFig.LogEvent("level_complete"); // Called 12 times
AppFig.GetFeatureValue("veteran_bonus"); // ← What happens now?
```

### Evaluation Process

**Step 1: Find the feature's rules**

```
SDK looks up "veteran_bonus" in its rules dictionary
Found: 1 rule to evaluate
```

**Step 2: Loop through rules (first match wins)**

```
Evaluating Rule #1...
```

**Step 3: Check event conditions**

```
Event condition: "level_complete" count >= 10

SDK counts matching events in history:
- Found 12 "level_complete" events
- Compare: 12 >= 10? ✓ YES

Event conditions: PASS ✓
```

**Step 4: Check user property conditions**

```
User property conditions: [] (empty)
No conditions to check

User property conditions: PASS ✓
```

**Step 5: Check device conditions**

```
Device conditions: [] (empty)
No conditions to check

Device conditions: PASS ✓
```

**Step 6: All conditions passed!**

```
✓ Events: PASS
✓ User Properties: PASS
✓ Device: PASS

Rule #1 MATCHES!
Return value: "2x"
```

**Result:**

```csharp
string bonus = AppFig.GetFeatureValue("veteran_bonus");
// bonus = "2x"
```

## Event History

The SDK maintains an in-memory list of all events you log:

```csharp
AppFig.LogEvent("level_complete"); // Event #1
AppFig.LogEvent("purchase_made");  // Event #2
AppFig.LogEvent("level_complete"); // Event #3
```

**Stored as:**

```
Event History = [
  { name: "level_complete", timestamp: 2025-01-15T10:00:00Z, params: {} },
  { name: "purchase_made",  timestamp: 2025-01-15T10:05:00Z, params: {} },
  { name: "level_complete", timestamp: 2025-01-15T10:10:00Z, params: {} }
]
```

When a rule checks `"level_complete" count >= 2`, the SDK:
1. Filters history for events named "level_complete"
2. Counts them (2 events found)
3. Compares: 2 >= 2? Yes ✓

## Event Modes

### Simple Mode (AND/OR)

Checks if events exist with certain criteria.

**Example: User completed 10+ levels AND made a purchase**

```json
{
  "events": {
    "mode": "simple",
    "operator": "AND",
    "items": [
      {
        "key": "level_complete",
        "count": { "operator": ">=", "value": 10 }
      },
      {
        "key": "purchase_made",
        "count": { "operator": ">=", "value": 1 }
      }
    ]
  }
}
```

**Evaluation:**
```
Check: level_complete count >= 10? (found 12) ✓
Check: purchase_made count >= 1?   (found 1)  ✓
Both pass → Rule matches!
```

### Sequence Mode (Direct/Indirect)

Checks if events occurred in a specific order.

**Example: User completed tutorial → made purchase → completed level**

```json
{
  "events": {
    "mode": "sequence",
    "ordering": "direct",
    "steps": [
      { "key": "tutorial_complete" },
      { "key": "purchase_made" },
      { "key": "level_complete" }
    ]
  }
}
```

**Direct ordering:** Events must occur **consecutively** (no other events between them)

```
Event history:
1. tutorial_complete ← Step 1 ✓
2. purchase_made     ← Step 2 ✓
3. level_complete    ← Step 3 ✓

Sequence matches!
```

**Indirect ordering:** Events must occur **in order** but can have other events between

```
Event history:
1. tutorial_complete ← Step 1 ✓
2. app_open         (ignored)
3. settings_changed  (ignored)
4. purchase_made     ← Step 2 ✓
5. level_complete    ← Step 3 ✓

Sequence matches!
```

## Property Matching

### User Properties

Set by your app, checked against rules:

```csharp
AppFig.SetUserProperty("subscription_tier", "premium");
```

**Rule checks:**
```json
{
  "user_properties": [
    {
      "key": "subscription_tier",
      "value": { "operator": "==", "value": "premium" }
    }
  ]
}
```

**Evaluation:**
```
Lookup: user_properties["subscription_tier"] → "premium"
Compare: "premium" == "premium" → ✓ PASS
```

### Device Properties

Auto-detected by SDK, no setup needed:

```
Device properties (auto-populated):
{
  "platform": "iOS",
  "os_version": "17.0",
  "language": "en-US",
  "country": "US",
  "device_brand": "Apple",
  "device_model": "iPhone 15"
}
```

**Rule checks:**
```json
{
  "device": [
    {
      "key": "platform",
      "value": { "operator": "in", "value": ["iOS", "Android"] }
    }
  ]
}
```

**Evaluation:**
```
Lookup: device["platform"] → "iOS"
Compare: "iOS" in ["iOS", "Android"] → ✓ PASS
```

## Time Windows

Events can be filtered by time:

```json
{
  "key": "level_complete",
  "count": { "operator": ">=", "value": 5 },
  "within_last_days": 7
}
```

**Evaluation:**
```
1. Filter events: name == "level_complete"
   Found: 12 events

2. Filter by time: timestamp >= (now - 7 days)
   Found: 3 events (9 were older than 7 days)

3. Check count: 3 >= 5? ✗ FAIL

Rule does not match
```

## Operators

### Comparison Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `==` | Equals | "premium" == "premium" |
| `!=` | Not equals | "free" != "premium" |
| `>` | Greater than | 10 &gt; 5 |
| `<` | Less than | 3 &lt; 10 |
| `>=` | Greater or equal | 10 &gt;= 10 |
| `<=` | Less or equal | 5 &lt;= 10 |

### Array Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `in` | Value in array | "iOS" in ["iOS", "Android"] |
| `not_in` | Value not in array | "Windows" not_in ["iOS", "Android"] |

### String Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `contains` | String contains substring | "hello world" contains "world" |

## Negation (NOT)

Any condition can be negated with `"not": true`:

```json
{
  "key": "purchase_made",
  "count": { "operator": ">=", "value": 1 },
  "not": true
}
```

**Evaluation:**
```
1. Check condition: purchase_made count >= 1? (found 0) → FAIL
2. Apply NOT: !FAIL → ✓ PASS

Matches users who have NOT made a purchase
```

## Performance: Why It's Fast

AppFig is optimized for high-performance client-side evaluation:

- **In-Memory Processing** - Rules, events, and properties are kept in memory for fast access
- **Smart Caching** - Feature values are cached after evaluation and invalidated only when relevant data changes
- **Efficient Evaluation** - The rule engine is optimized to minimize computational overhead

These optimizations ensure feature checks are extremely fast, with subsequent checks being nearly instantaneous due to caching.

## Rule Evaluation Order

Rules are evaluated in the order you define them. **First match wins.**

```json
Feature: "discount"
Rules: [
  Rule 1: IF premium user → "50% off"
  Rule 2: IF any user → "10% off"
]
```

**Premium user evaluation:**
```
Check Rule 1: premium user? ✓ YES
→ Return "50% off"
(Rule 2 never evaluated)
```

**Free user evaluation:**
```
Check Rule 1: premium user? ✗ NO
Check Rule 2: any user? ✓ YES
→ Return "10% off"
```

## What Gets Sent to the Server?

**Short answer: Almost nothing.**

### Cloud Mode (Paid)
When you initialize:
```
SDK → Server: "Give me latest rules for my tenant"
Server → SDK: Rules JSON (happens once, then cached)
```

When you call `GetFeatureValue()`:
```
No network call! Everything evaluated locally.
```

Optional usage tracking (sampled, ~0.1% of requests):
```
SDK → Server: "Increment fetch counter" (no event data, no user data)
```

### Local Mode (Free)
```
No network calls at all. Rules loaded from JSON file in app.
```

## Next Steps

- **[Rule Schema Reference](#AppFig-docs%2FReference%2Frule-schema.md)** - All rule options and operators
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your rules


---

<!-- SOURCE: AppFig-docs/SDK-Documentation/android-integration.md -->

# Android SDK Integration Guide

Complete guide to integrating AppFig into your Android application.

## Installation

> **SDK Download Repository:** [https://github.com/smartconfigui/AppFig-SDKs](https://github.com/smartconfigui/AppFig-SDKs)

**Requirements:** Android API 21+ (Android 5.0 Lollipop), Android Studio 4.0+, Gradle 7.0+

### Step 1 — Copy the AAR

Place the SDK file into your app module:

```
app/libs/AppFigAndroidSDK.aar
```

Create the folder if it doesn't exist.

### Step 2 — Add Dependencies

**If your app uses Groovy (build.gradle):**

```gradle
dependencies {
    // AppFig SDK
    implementation files('libs/AppFigAndroidSDK.aar')

    // Required dependencies
    implementation "com.squareup.okhttp3:okhttp:4.12.0"
    implementation "com.google.code.gson:gson:2.10.1"
}
```

**If your app uses Kotlin DSL (build.gradle.kts):**

```kotlin
dependencies {
    // AppFig SDK
    implementation(files("libs/AppFigAndroidSDK.aar"))

    // Required dependencies
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.google.code.gson:gson:2.10.1")
}
```

### Step 3 — Add Permissions

Add internet permissions to your `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
```

### Step 4 — Sync Gradle

Android Studio → "Sync Project with Gradle Files".

## Quick Start

### Cloud Mode (Paid Plans)

```kotlin
import com.appfig.sdk.AppFig

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // Initialize AppFig with auto-refresh
        AppFig.init(
            context = this,
            companyId = "your-company-id",
            tenantId = "your-tenant-id",
            env = "prod",
            apiKey = "your-api-key",
            autoRefresh = true,
            pollInterval = 3600000  // 1 hour
        )

        // Log events
        AppFig.logEvent("app_open")

        // Check features
        if (AppFig.isFeatureEnabled("double_xp")) {
            applyDoubleXP()
        }

        val maxLives = AppFig.getFeatureValue("max_lives")?.toIntOrNull() ?: 3
    }
}
```

### Local Mode (Free Plan)

```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // Load rules from assets/appfig_rules.json
        val rulesJson = assets.open("appfig_rules.json").bufferedReader().use { it.readText() }
        AppFig.initLocal(this, rulesJson)

        // Use features exactly the same way
        if (AppFig.isFeatureEnabled("tutorial_mode")) {
            startTutorial()
        }
    }
}
```

## When to Do What: Timing Guide

Understanding WHEN to call each SDK method is crucial for proper Android integration:

| Action | When to Call | Where to Place | Code Example |
|--------|-------------|----------------|--------------|
| **Initialize SDK** | Once at app startup | `Application.onCreate()` | `AppFig.init(...)` |
| **Log Events** | Immediately after user action | onClick listeners, lifecycle methods | `AppFig.logEvent("button_click")` |
| **Set User Properties** | Once at login, when value changes | Auth callback, state change | `AppFig.setUserProperty("tier", "premium")` |
| **Evaluate Features** | Right before you need the value | onCreate, onResume, before decisions | `AppFig.getFeatureValue("max_lives")` |
| **Apply Features** | Immediately after evaluation | Same method as evaluation | `if (enabled) showUI()` |
| **Refresh Rules** | On demand (auto-refresh handles this) | Manual: Settings screen | `AppFig.refreshRules()` |

### Complete Lifecycle Example

```kotlin
import android.app.Application
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.appfig.sdk.AppFig

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 1. INITIALIZE: Once at app startup
        AppFig.init(
            context = this,
            companyId = "your-company-id",
            tenantId = "your-tenant-id",
            env = "prod",
            apiKey = "your-api-key"
        )

        // 2. TRACK: Log app open
        AppFig.logEvent("app_open")
    }
}

class MainActivity : AppCompatActivity() {
    private var playerLives = 3

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 3. EVALUATE: Check features when activity created
        checkWelcomeBonus()
        configureGameSettings()
    }

    private fun checkWelcomeBonus() {
        // Evaluate right before using
        if (AppFig.isFeatureEnabled("welcome_bonus")) {
            // 4. APPLY: Show bonus immediately
            findViewById<View>(R.id.welcomePanel).visibility = View.VISIBLE
            grantBonus(500)
        } else {
            findViewById<View>(R.id.welcomePanel).visibility = View.GONE
        }
    }

    private fun configureGameSettings() {
        // Evaluate with fallback
        val maxLives = AppFig.getFeatureValue("max_lives")?.toIntOrNull() ?: 3
        playerLives = maxLives

        // Apply to UI
        findViewById<TextView>(R.id.livesText).text = "Lives: $playerLives"
    }

    fun onLevelComplete(view: View) {
        // TRACK: Log immediately after action
        AppFig.logEvent("level_complete", mapOf(
            "level" to currentLevel.toString()
        ))

        // Re-evaluate features that depend on level
        checkNewUnlocks()
    }

    fun onSubscriptionPurchased(tier: String) {
        // Update property when it changes
        AppFig.setUserProperty("subscription_tier", tier)

        // Re-evaluate premium features
        updatePremiumFeatures()
    }
}
```

### Timing Best Practices

#### Initialize Once in Application Class

```kotlin
// ✅ CORRECT: Initialize in Application.onCreate()
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        AppFig.init(context = this, companyId = "...", tenantId = "...", env = "prod", apiKey = "...")
    }
}

// ❌ WRONG: Don't initialize in every activity
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AppFig.init(...)  // Bad! SDK already initialized
    }
}
```

#### Log Events Immediately

```kotlin
// ✅ CORRECT: Log right when action happens
fun onPurchaseClicked(view: View) {
    AppFig.logEvent("purchase_initiated")
    showPurchaseDialog()
}

// ❌ WRONG: Don't batch events
private val pendingEvents = mutableListOf<String>()

fun onLevelComplete() {
    pendingEvents.add("level_complete")  // Bad!
}
```

#### Cache Feature Values

```kotlin
// ✅ CORRECT: Check once and cache
class GameActivity : AppCompatActivity() {
    private var hasDoubleXP = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        hasDoubleXP = AppFig.isFeatureEnabled("double_xp")
    }

    fun calculateXP(baseXP: Int): Int {
        return if (hasDoubleXP) baseXP * 2 else baseXP
    }
}

// ❌ WRONG: Don't check repeatedly
fun update() {
    if (AppFig.isFeatureEnabled("double_xp")) {  // Bad!
        applyDoubleXP()
    }
}
```

---

## API Reference

### Initialization

#### Cloud Mode

```kotlin
AppFig.init(
    context: Context,                     // Required: Application context
    companyId: String,                    // Required: Your company ID
    tenantId: String,                     // Required: Your tenant/project ID
    env: String,                          // Required: "dev" or "prod"
    apiKey: String,                       // Required: Your API key
    autoRefresh: Boolean = true,          // Optional: Enable auto-refresh (default: true) ✨ NEW DEFAULT
    pollInterval: Long = 43200000L,       // Optional: 12 hours (default)
    debugMode: Boolean = false,           // Optional: Enable debug logging (default: false) ✨ NEW
    sessionTimeoutMs: Long = 1800000L,    // Optional: 30 minutes (default: 30 min) ✨ NEW
    maxEvents: Int = 5000,                // Optional: Max events (default: 5000, max: 100000)
    maxEventAgeDays: Int = 7              // Optional: Max age in days (default: 7, max: 365)
)
```

**Parameters:**
- `context` (required) - Application context (use `this` from Application.onCreate() or activity)
- `companyId` (required) - Your company ID from AppFig dashboard
- `tenantId` (required) - Your tenant/project ID from AppFig dashboard
- `env` (required) - Environment: `"dev"` or `"prod"`
- `apiKey` (required) - Your API key from AppFig dashboard
- `autoRefresh` (optional) - Enable automatic background rule updates (default: **true**) ⚠️ **Breaking Change**
- `pollInterval` (optional) - Auto-refresh interval in milliseconds (default: 43200000L = 12 hours, min: server-enforced)
- `debugMode` (optional) - Enable debug logging to Logcat (default: false) 🆕 **New in v2.0**
- `sessionTimeoutMs` (optional) - Session timeout in milliseconds (default: 1800000L = 30 min, range: 60000-7200000) 🆕 **New in v2.0**
- `maxEvents` (optional) - Maximum events to store in SharedPreferences (100-100000, default: 5000)
- `maxEventAgeDays` (optional) - Maximum age of events in days (1-365, default: 7)

**Examples:**

```kotlin
// Basic initialization (auto-refresh enabled by default)
AppFig.init(this, "acme-games", "space-shooter", "prod", "sk_live_abc123")

// With custom poll interval (1 hour instead of 12 hours)
AppFig.init(
    context = this,
    companyId = "acme-games",
    tenantId = "space-shooter",
    env = "prod",
    apiKey = "sk_live_abc123",
    autoRefresh = true,          // Explicit (but this is already the default)
    pollInterval = 3600000L      // 1 hour
)

// With debug mode enabled (useful during development)
AppFig.init(
    context = this,
    companyId = "acme-games",
    tenantId = "space-shooter",
    env = "dev",                 // Use dev environment
    apiKey = "sk_test_abc123",
    autoRefresh = true,
    pollInterval = 600000L,      // 10 minutes for faster testing
    debugMode = true             // See all SDK operations in Logcat
)

// With all parameters customized
AppFig.init(
    context = this,
    companyId = "acme-games",
    tenantId = "space-shooter",
    env = "prod",
    apiKey = "sk_live_abc123",
    autoRefresh = true,
    pollInterval = 3600000L,
    debugMode = false,
    sessionTimeoutMs = 900000L,  // 15 minutes instead of 30
    maxEvents = 10000,           // Store more events
    maxEventAgeDays = 30         // Keep events for 30 days
)

// Disable auto-refresh (opt-out, manual refresh only)
AppFig.init(
    context = this,
    companyId = "acme-games",
    tenantId = "space-shooter",
    env = "prod",
    apiKey = "sk_live_abc123",
    autoRefresh = false          // Must call refreshRules() manually
)
```

#### Local Mode

```kotlin
AppFig.initLocal(context: Context, rulesJson: String? = null)
```

**Parameters:**
- `context` - Application context
- `rulesJson` (optional) - JSON string with rules. If `null`, no rules are loaded automatically — `initLocal()` logs a warning and all features return `null` until you call `updateLocalRules()`.

**Example:**
```kotlin
// Load from assets/appfig_rules.json
val rulesJson = assets.open("appfig_rules.json").bufferedReader().use { it.readText() }
AppFig.initLocal(this, rulesJson)

// Calling without rulesJson loads nothing — features return null until
// updateLocalRules() is called
AppFig.initLocal(this)
```

### Logging Events

```kotlin
AppFig.logEvent(eventName: String, parameters: Map<String, String>? = null)
```

**Examples:**
```kotlin
// Simple event
AppFig.logEvent("level_complete")

// Event with parameters
AppFig.logEvent("purchase_made", mapOf(
    "item_id" to "sword_legendary",
    "price" to "9.99",
    "currency" to "USD"
))

// Screen view (auto-tracked by SDK too)
AppFig.logScreenView("main_menu", "settings")
```

### Checking Features

```kotlin
AppFig.isFeatureEnabled(feature: String): Boolean
AppFig.getFeatureValue(feature: String): String?
```

**Examples:**
```kotlin
// Boolean check (returns true if value == "on")
if (AppFig.isFeatureEnabled("new_ui")) {
    loadNewUI()
} else {
    loadOldUI()
}

// Get actual value
val maxLives = AppFig.getFeatureValue("max_lives")
if (maxLives != null) {
    playerLives = maxLives.toIntOrNull() ?: 3
}

// Handle null (no matching rules)
val bonus = AppFig.getFeatureValue("veteran_bonus")
if (bonus == null) {
    Log.d("AppFig", "No bonus active")
} else {
    applyBonus(bonus) // "2x", "1.5x", etc.
}
```

### Setting Properties

```kotlin
// User properties
AppFig.setUserProperty(key: String, value: String)
AppFig.removeUserProperty(key: String)

// Device properties
AppFig.setDeviceProperty(key: String, value: String)
AppFig.removeDeviceProperty(key: String)

// App version
AppFig.setAppVersion(version: String)
```

**Examples:**
```kotlin
// User properties
AppFig.setUserProperty("subscription_tier", "premium")
AppFig.setUserProperty("account_age_days", "30")
AppFig.setUserProperty("player_level", "15")

// Device properties (most are auto-detected)
AppFig.setAppVersion("1.2.3")
AppFig.setDeviceProperty("graphics_quality", "high")

// Remove properties
AppFig.removeUserProperty("subscription_tier")
```

### Auto-Detected Device Properties

These are automatically set by the SDK on initialization:

```
platform          "Android"
os_version        "13", "12", "11"
language          "English", "Japanese", "Spanish"
timezone          "America/Los_Angeles", "Asia/Tokyo"
country           "US", "JP", "GB" (requires internet on first run)
device_brand      "Samsung", "Google", "Huawei", "OnePlus"
device_model      "SM-G998B", "Pixel 8", "OnePlus 11"
```

### Session Tracking

Automatically tracked:

```kotlin
// These events are logged automatically:
"first_open"     // First time app opens (persisted in SharedPreferences)
"session_start"  // App comes to foreground
"session_end"    // App goes to background, or `sessionTimeoutMs` of inactivity (default: 30 min, range: 1 min - 2 hr)
"screen_view"    // When you call logScreenView()
```

### Manual Refresh

```kotlin
AppFig.refreshRules()  // Manually reload rules from CDN
```

**Example:**
```kotlin
// Manually refresh rules (useful for pull-to-refresh)
AppFig.refreshRules()
```

**Note:** `setAutoRefresh()` and `setPollInterval()` are deprecated. Use the `autoRefresh` and `pollInterval` parameters in `init()` instead.

### Feature Reset (Recurring Triggers)

```kotlin
AppFig.resetFeature(featureName: String)
AppFig.resetAllFeatures()
```

Reset a feature's cached value and force re-evaluation. Useful for implementing recurring triggers like "show popup every 3 events".

**Example:**
```kotlin
// Check if feature is enabled
if (AppFig.isFeatureEnabled("level_complete_popup")) {
    showPopup("You've completed 3 levels!")

    // Reset so it triggers again after next 3 events
    AppFig.resetFeature("level_complete_popup")
}

// With event logging
fun onLevelComplete(level: Int) {
    AppFig.logEvent("level_complete", mapOf("level" to level.toString()))

    // Check for milestone reward
    if (AppFig.isFeatureEnabled("milestone_reward")) {
        showReward()
        AppFig.resetFeature("milestone_reward")
    }
}
```

**Use Cases:**
- Show popup every N events (e.g., every 3 level completions)
- Recurring promotions after X purchases
- Periodic reward unlocks
- Time-based feature re-triggers

### Debugging

```kotlin
AppFig.hasRulesLoaded(): Boolean
AppFig.getEventHistoryStats(): Map<String, Any>
```

**Example:**
```kotlin
if (AppFig.hasRulesLoaded()) {
    Log.d("AppFig", "Rules are loaded")
}

val stats = AppFig.getEventHistoryStats()
Log.d("AppFig", "Event count: ${stats["count"]}")
Log.d("AppFig", "Size: ${stats["sizeMB"]} MB")
```

## Best Practices

### 1. Initialize in Application Class

```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        AppFig.init(
            context = this,
            companyId = "company",
            tenantId = "tenant",
            env = "prod",
            apiKey = "api-key",
            autoRefresh = true
        )
    }
}
```

Don't forget to register in `AndroidManifest.xml`:
```xml
<application
    android:name=".MyApplication"
    ...>
```

### 2. Cache Feature Values

```kotlin
// ❌ Bad: Checks every frame
override fun onDraw(canvas: Canvas) {
    if (AppFig.isFeatureEnabled("double_xp")) {
        // ...
    }
}

// ✅ Good: Check once, store result
private val hasDoubleXP by lazy { AppFig.isFeatureEnabled("double_xp") }

override fun onDraw(canvas: Canvas) {
    if (hasDoubleXP) {
        // ...
    }
}
```

### 3. Handle Null Values

```kotlin
val maxLives = AppFig.getFeatureValue("max_lives")?.toIntOrNull() ?: 5 // fallback
```

### 4. Log Events Liberally

```kotlin
fun completeLevel(level: Int, stars: Int, timeSeconds: Int) {
    AppFig.logEvent("level_complete", mapOf(
        "level_number" to level.toString(),
        "time_seconds" to timeSeconds.toString(),
        "stars" to stars.toString()
    ))
}
```

## Troubleshooting

### AAR not recognized by Android Studio

**Problem:** Android Studio doesn't find AppFig classes

**Solutions:**
1. Verify `AppFigAndroidSDK.aar` is in `app/libs/` folder
2. Check `build.gradle` has `implementation files('libs/AppFigAndroidSDK.aar')`
3. Sync Gradle files (File → Sync Project with Gradle Files)
4. Clean and rebuild project (Build → Clean Project, then Build → Rebuild Project)

### "AppFig not initialized"

**Problem:** Calling SDK methods before `init()` or `initLocal()`

**Solution:**
```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        AppFig.init(...)  // Must be first!
        AppFig.logEvent("app_open")  // Now OK
    }
}
```

### Feature always returns null

**Problem:** No rules match for this feature

**Solutions:**
1. Check feature name spelling (case-sensitive)
2. Verify rules are loaded: `AppFig.hasRulesLoaded()`
3. Log relevant events/properties before checking feature
4. Check Logcat for AppFig debug messages

### Rules not updating (Cloud Mode)

**Problem:** Changes in dashboard not reflected in app

**Solutions:**
1. Wait for poll interval (default: 12 hours)
2. Call `AppFig.refreshRules()` manually
3. Check Logcat for "AppFig" tag messages
4. Verify internet connection

### Rules not loading (Local Mode)

**Problem:** `Failed to load rules from local JSON`

**Solutions:**
1. Verify file exists at `assets/appfig_rules.json`
2. Check JSON syntax is valid
3. Ensure file is included in APK

## Performance Tips

### Event History Management

The SDK automatically manages event history to maintain optimal performance. Events are automatically trimmed based on:
- Maximum event count (default: 5000)
- Maximum event age (default: 7 days)

### Memory and CPU Usage

AppFig has minimal memory and CPU overhead:
- Low memory footprint suitable for mobile devices
- Fast feature checks with intelligent caching
- Efficient rule evaluation

## Next Steps

- **[Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)** - All rule options
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your features
- **[Local Mode](#AppFig-docs%2FAdvanced%2Flocal-mode.md)** - Detailed local mode guide


---

<!-- SOURCE: AppFig-docs/SDK-Documentation/ios-integration.md -->

# iOS SDK Integration Guide

Complete guide to integrating AppFig into your iOS application.

## Installation

> **SDK Download Repository:** [https://github.com/smartconfigui/AppFig-SDKs](https://github.com/smartconfigui/AppFig-SDKs)

**Requirements:** iOS 13.0+, Xcode 12.0+, Swift 5.0+

### Step 1: Download the SDK

Download `AppFig.xcframework` from:
```
https://github.com/smartconfigui/AppFig-SDKs/tree/main/ios
```

### Step 2: Add to Xcode Project

1. Drag the entire `AppFig.xcframework` folder into your Xcode project navigator
2. In the dialog that appears, check "Copy items if needed"
3. Click "Finish"

### Step 3: Configure Framework Embedding

1. Select your project in the Project Navigator
2. Select your app target
3. Go to the "General" tab
4. Scroll to "Frameworks, Libraries, and Embedded Content"
5. Find `AppFig.xcframework` in the list
6. Set it to **"Embed & Sign"** (not "Do Not Embed")

### Step 4: Import in Your Code

```swift
import AppFig
```

**Note:** No external dependencies required. The XCFramework is self-contained.

## Quick Start

### Cloud Mode (Paid Plans)

```swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Initialize AppFig with auto-refresh
        AppFig.initialize(
            companyId: "your-company-id",
            tenantId: "your-tenant-id",
            env: "prod",
            apiKey: "your-api-key",
            autoRefresh: true,
            pollInterval: 3600000  // 1 hour
        )

        // Log events
        AppFig.logEvent(name: "app_open")

        // Check features
        if AppFig.isFeatureEnabled("double_xp") {
            applyDoubleXP()
        }

        if let maxLives = AppFig.getFeatureValue("max_lives") {
            playerLives = Int(maxLives) ?? 3
        }

        return true
    }
}
```

### Local Mode (Free Plan)

```swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Load rules from bundle
        if let path = Bundle.main.path(forResource: "appfig_rules", ofType: "json"),
           let rulesJson = try? String(contentsOfFile: path) {
            AppFig.initializeLocal(rulesJson: rulesJson)
        }

        // Use features exactly the same way
        if AppFig.isFeatureEnabled("tutorial_mode") {
            startTutorial()
        }

        return true
    }
}
```

## When to Do What: Timing Guide

Understanding WHEN to call each SDK method is crucial for proper iOS integration:

| Action | When to Call | Where to Place | Code Example |
|--------|-------------|----------------|--------------|
| **Initialize SDK** | Once at app launch | `didFinishLaunchingWithOptions` | `AppFig.initialize(...)` |
| **Log Events** | Immediately after user action | IBAction methods, delegates | `AppFig.logEvent(name: "button_tap")` |
| **Set User Properties** | Once at login, when value changes | Auth callback, state change | `AppFig.setUserProperty(key: "tier", value: "premium")` |
| **Evaluate Features** | Right before you need the value | viewDidLoad, viewWillAppear | `AppFig.getFeatureValue("max_lives")` |
| **Apply Features** | Immediately after evaluation | Same method as evaluation | `if enabled { showUI() }` |
| **Refresh Rules** | On demand (auto-refresh handles this) | Manual: Settings screen | `AppFig.refreshRules()` |

### Complete Lifecycle Example

```swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // 1. INITIALIZE: Once at app launch
        AppFig.initialize(
            companyId: "your-company-id",
            tenantId: "your-tenant-id",
            env: "prod",
            apiKey: "your-api-key"
        )

        // 2. TRACK: Log app open event
        AppFig.logEvent(name: "app_open")

        return true
    }
}

class MainViewController: UIViewController {
    @IBOutlet weak var welcomePanel: UIView!
    @IBOutlet weak var livesLabel: UILabel!
    private var playerLives = 3

    override func viewDidLoad() {
        super.viewDidLoad()

        // 3. EVALUATE: Check features when view loads
        checkWelcomeBonus()
        configureGameSettings()
    }

    func checkWelcomeBonus() {
        // Evaluate right before using
        if AppFig.isFeatureEnabled("welcome_bonus") {
            // 4. APPLY: Show bonus immediately
            welcomePanel.isHidden = false
            grantBonus(coins: 500)
        } else {
            welcomePanel.isHidden = true
        }
    }

    func configureGameSettings() {
        // Evaluate with fallback
        if let maxLivesStr = AppFig.getFeatureValue("max_lives"),
           let maxLives = Int(maxLivesStr) {
            playerLives = maxLives
        } else {
            playerLives = 3  // Fallback
        }

        // Apply to UI
        livesLabel.text = "Lives: \(playerLives)"
    }

    @IBAction func onLevelComplete(_ sender: UIButton) {
        // TRACK: Log immediately after action
        AppFig.logEvent(name: "level_complete", parameters: [
            "level": "\(currentLevel)"
        ])

        // Re-evaluate features that depend on level
        checkNewUnlocks()
    }

    func onSubscriptionPurchased(tier: String) {
        // Update property when it changes
        AppFig.setUserProperty(key: "subscription_tier", value: tier)

        // Re-evaluate premium features
        updatePremiumFeatures()
    }
}
```

### Timing Best Practices

#### Initialize Once at Launch

```swift
// ✅ CORRECT: Initialize in app delegate
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    AppFig.initialize(companyId: "...", tenantId: "...", env: "prod", apiKey: "...")
    return true
}

// ❌ WRONG: Don't initialize in every view controller
class MyViewController: UIViewController {
    override func viewDidLoad() {
        AppFig.initialize(...)  // Bad! SDK already initialized
    }
}
```

#### Log Events Immediately

```swift
// ✅ CORRECT: Log right when action happens
@IBAction func purchaseButtonTapped(_ sender: UIButton) {
    AppFig.logEvent(name: "purchase_initiated")
    showPurchaseDialog()
}

// ❌ WRONG: Don't batch events
var pendingEvents: [String] = []

func onLevelComplete() {
    pendingEvents.append("level_complete")  // Bad!
}
```

#### Cache Feature Values

```swift
// ✅ CORRECT: Check once and cache
class GameViewController: UIViewController {
    private var hasDoubleXP = false

    override func viewDidLoad() {
        super.viewDidLoad()
        hasDoubleXP = AppFig.isFeatureEnabled("double_xp")
    }

    func calculateXP(_ baseXP: Int) -> Int {
        return hasDoubleXP ? baseXP * 2 : baseXP
    }
}

// ❌ WRONG: Don't check repeatedly
func update() {
    if AppFig.isFeatureEnabled("double_xp") {  // Bad!
        applyDoubleXP()
    }
}
```

---

## API Reference

### Initialization

#### Cloud Mode

```swift
AppFig.initialize(
    companyId: String,                         // Required: Your company ID
    tenantId: String,                          // Required: Your tenant/project ID
    env: String,                               // Required: "dev" or "prod"
    apiKey: String,                            // Required: Your API key
    autoRefresh: Bool = true,                  // Optional: Enable auto-refresh (default: true) ✨ NEW DEFAULT
    pollInterval: TimeInterval = 43200000,     // Optional: 12 hours (default)
    debugMode: Bool = false,                   // Optional: Enable debug logging (default: false) ✨ NEW
    sessionTimeoutMs: Int64 = 1800000,         // Optional: 30 minutes (default: 30 min) ✨ NEW
    maxEvents: Int = 5000,                     // Optional: Max events (default: 5000, max: 100000)
    maxEventAgeDays: Int = 7                   // Optional: Max age in days (default: 7, max: 365)
)
```

**Parameters:**
- `companyId` (required) - Your company ID from AppFig dashboard
- `tenantId` (required) - Your tenant/project ID from AppFig dashboard
- `env` (required) - Environment: `"dev"` or `"prod"`
- `apiKey` (required) - Your API key from AppFig dashboard
- `autoRefresh` (optional) - Enable automatic background rule updates (default: **true**) ⚠️ **Breaking Change**
- `pollInterval` (optional) - Auto-refresh interval in milliseconds (default: 43200000 = 12 hours, min: server-enforced)
- `debugMode` (optional) - Enable debug logging to console (default: false) 🆕 **New in v2.0**
- `sessionTimeoutMs` (optional) - Session timeout in milliseconds (default: 1800000 = 30 min, range: 60000-7200000) 🆕 **New in v2.0**
- `maxEvents` (optional) - Maximum events to store in UserDefaults (100-100000, default: 5000)
- `maxEventAgeDays` (optional) - Maximum age of events in days (1-365, default: 7)

**Examples:**

```swift
// Basic initialization (auto-refresh enabled by default)
AppFig.initialize(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123"
)

// With custom poll interval (1 hour instead of 12 hours)
AppFig.initialize(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123",
    autoRefresh: true,          // Explicit (but this is already the default)
    pollInterval: 3600000       // 1 hour
)

// With debug mode enabled (useful during development)
AppFig.initialize(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "dev",                 // Use dev environment
    apiKey: "sk_test_abc123",
    autoRefresh: true,
    pollInterval: 600000,       // 10 minutes for faster testing
    debugMode: true             // See all SDK operations in console
)

// With all parameters customized
AppFig.initialize(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123",
    autoRefresh: true,
    pollInterval: 3600000,
    debugMode: false,
    sessionTimeoutMs: 900000,   // 15 minutes instead of 30
    maxEvents: 10000,           // Store more events
    maxEventAgeDays: 30         // Keep events for 30 days
)

// Disable auto-refresh (opt-out, manual refresh only)
AppFig.initialize(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123",
    autoRefresh: false          // Must call refreshRules() manually
)
```

#### Local Mode

```swift
AppFig.initializeLocal(rulesJson: String? = nil)
```

**Parameters:**
- `rulesJson` (optional) - JSON string with rules. If `nil`, no rules are loaded automatically — `initializeLocal()` logs a warning and all features return `nil` until rules are provided.

**Example:**
```swift
// Load from bundle
if let path = Bundle.main.path(forResource: "appfig_rules", ofType: "json"),
   let rulesJson = try? String(contentsOfFile: path) {
    AppFig.initializeLocal(rulesJson: rulesJson)
}
```

### Logging Events

```swift
AppFig.logEvent(name: String, parameters: [String: String]? = nil)
```

**Examples:**
```swift
// Simple event
AppFig.logEvent(name: "level_complete")

// Event with parameters
AppFig.logEvent(name: "purchase_made", parameters: [
    "item_id": "sword_legendary",
    "price": "9.99",
    "currency": "USD"
])

// Screen view (auto-tracked by SDK too)
AppFig.logScreenView(screenName: "main_menu", previousScreen: "settings")
```

### Checking Features

```swift
AppFig.isFeatureEnabled(_ feature: String) -> Bool
AppFig.getFeatureValue(_ feature: String) -> String?
```

**Examples:**
```swift
// Boolean check (returns true if value == "on")
if AppFig.isFeatureEnabled("new_ui") {
    loadNewUI()
} else {
    loadOldUI()
}

// Get actual value
if let maxLives = AppFig.getFeatureValue("max_lives") {
    playerLives = Int(maxLives) ?? 3
}

// Handle nil (no matching rules)
if let bonus = AppFig.getFeatureValue("veteran_bonus") {
    applyBonus(bonus) // "2x", "1.5x", etc.
} else {
    print("No bonus active")
}
```

### Setting Properties

```swift
// User properties
AppFig.setUserProperty(key: String, value: String)
AppFig.removeUserProperty(key: String)

// Device properties
AppFig.setDeviceProperty(key: String, value: String)
AppFig.removeDeviceProperty(key: String)

// App version
AppFig.setAppVersion(_ version: String)
```

**Examples:**
```swift
// User properties
AppFig.setUserProperty(key: "subscription_tier", value: "premium")
AppFig.setUserProperty(key: "account_age_days", value: "30")
AppFig.setUserProperty(key: "player_level", value: "15")

// Device properties (most are auto-detected)
AppFig.setAppVersion("1.2.3")
AppFig.setDeviceProperty(key: "graphics_quality", value: "high")

// Remove properties
AppFig.removeUserProperty(key: "subscription_tier")
```

### Auto-Detected Device Properties

These are automatically set by the SDK on initialization:

```
platform          "iOS", "iPadOS"
os_version        "17.0", "16.5", "15.0"
language          "English", "Japanese", "Spanish"
timezone          "America/Los_Angeles", "Asia/Tokyo"
country           "US", "JP", "GB" (requires internet on first run)
device_brand      "Apple"
device_model      "iPhone 15 Pro", "iPad Air", "iPhone SE"
```

### Session Tracking

Automatically tracked:

```swift
// These events are logged automatically:
"first_open"     // First time app opens (persisted in UserDefaults)
"session_start"  // App comes to foreground
"session_end"    // App goes to background, or `sessionTimeoutMs` of inactivity (default: 30 min, range: 1 min - 2 hr)
"screen_view"    // When you call logScreenView()
```

### Manual Refresh

```swift
AppFig.refreshRules()  // Manually reload rules from CDN
```

**Example:**
```swift
// Manually refresh rules (useful for pull-to-refresh)
AppFig.refreshRules()
```

**Note:** `setAutoRefresh()` and `setPollInterval()` are deprecated. Use the `autoRefresh` and `pollInterval` parameters in `initialize()` instead.

### Feature Reset (Recurring Triggers)

```swift
AppFig.resetFeature(_ featureName: String)
AppFig.resetAllFeatures()
```

Reset a feature's cached value and force re-evaluation. Useful for implementing recurring triggers like "show popup every 3 events".

**Example:**
```swift
// Check if feature is enabled
if AppFig.isFeatureEnabled("level_complete_popup") {
    showPopup(message: "You've completed 3 levels!")

    // Reset so it triggers again after next 3 events
    AppFig.resetFeature("level_complete_popup")
}

// With event logging
func onLevelComplete(level: Int) {
    AppFig.logEvent(name: "level_complete", parameters: ["level": String(level)])

    // Check for milestone reward
    if AppFig.isFeatureEnabled("milestone_reward") {
        showReward()
        AppFig.resetFeature("milestone_reward")
    }
}
```

**Use Cases:**
- Show popup every N events (e.g., every 3 level completions)
- Recurring promotions after X purchases
- Periodic reward unlocks
- Time-based feature re-triggers

### Debugging

```swift
AppFig.hasRulesLoaded() -> Bool
AppFig.getEventHistoryStats() -> [String: Any]
```

**Example:**
```swift
if AppFig.hasRulesLoaded() {
    print("Rules are loaded")
}

let stats = AppFig.getEventHistoryStats()
print("Event count: \(stats["count"] ?? 0)")
print("Size: \(stats["sizeMB"] ?? 0) MB")
```

## SwiftUI Integration

### App Entry Point

```swift
import SwiftUI

@main
struct MyApp: App {

    init() {
        // Initialize AppFig
        AppFig.initialize(
            companyId: "acme-games",
            tenantId: "my-app",
            env: "prod",
            apiKey: "sk_live_abc123",
            autoRefresh: true
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

### Feature Wrapper

```swift
struct FeatureView<Content: View>: View {
    let featureName: String
    let content: Content

    init(_ featureName: String, @ViewBuilder content: () -> Content) {
        self.featureName = featureName
        self.content = content()
    }

    var body: some View {
        if AppFig.isFeatureEnabled(featureName) {
            content
        }
    }
}

// Usage
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Welcome!")

            FeatureView("premium_banner") {
                PremiumBanner()
            }
        }
    }
}
```

## Best Practices

### 1. Initialize Early

Initialize in AppDelegate or App struct:

```swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        AppFig.initialize(
            companyId: "company",
            tenantId: "tenant",
            env: "prod",
            apiKey: "api-key",
            autoRefresh: true
        )
        return true
    }
}
```

### 2. Cache Feature Values

```swift
// ❌ Bad: Checks every frame
override func draw(_ rect: CGRect) {
    if AppFig.isFeatureEnabled("double_xp") {
        // ...
    }
}

// ✅ Good: Check once, store result
private lazy var hasDoubleXP = AppFig.isFeatureEnabled("double_xp")

override func draw(_ rect: CGRect) {
    if hasDoubleXP {
        // ...
    }
}
```

### 3. Handle Nil Values

```swift
let maxLives = Int(AppFig.getFeatureValue("max_lives") ?? "5") ?? 5
```

### 4. Log Events Liberally

```swift
func completeLevel(_ level: Int, stars: Int, timeSeconds: Int) {
    AppFig.logEvent(name: "level_complete", parameters: [
        "level_number": "\(level)",
        "time_seconds": "\(timeSeconds)",
        "stars": "\(stars)"
    ])
}
```

## Troubleshooting

### "Framework not found: AppFig"

**Problem:** Xcode can't find the AppFig framework

**Solutions:**
1. Verify `AppFig.xcframework` is in your project navigator
2. Check "Embed & Sign" is selected in Frameworks, Libraries, and Embedded Content
3. Clean build folder (Product → Clean Build Folder)
4. Restart Xcode

### Code signing issues

**Problem:** "Code signing failed" or "Framework not properly signed"

**Solutions:**
1. Ensure "Embed & Sign" is selected (not "Embed Without Signing")
2. Verify your provisioning profile includes framework embedding
3. Check code signing settings in Build Settings

### "AppFig not initialized"

**Problem:** Calling SDK methods before `initialize()` or `initializeLocal()`

**Solution:**
```swift
func application(_ application: UIApplication,
                didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    AppFig.initialize(...)  // Must be first!
    AppFig.logEvent(name: "app_open")  // Now OK
    return true
}
```

### Feature always returns nil

**Problem:** No rules match for this feature

**Solutions:**
1. Check feature name spelling (case-sensitive)
2. Verify rules are loaded: `AppFig.hasRulesLoaded()`
3. Log relevant events/properties before checking feature
4. Check console for AppFig debug messages

### Rules not updating (Cloud Mode)

**Problem:** Changes in dashboard not reflected in app

**Solutions:**
1. Enable auto-refresh in `initialize()` with desired poll interval
2. Call `AppFig.refreshRules()` manually
3. Check console for AppFig messages
4. Verify internet connection

### Rules not loading (Local Mode)

**Problem:** `Failed to load rules from local JSON`

**Solutions:**
1. Verify file exists in bundle
2. Check JSON syntax is valid
3. Ensure file is added to target

## Performance Tips

### Event History Management

The SDK automatically manages event history to maintain optimal performance. Events are automatically trimmed based on:
- Maximum event count (default: 5000)
- Maximum event age (default: 7 days)

### Memory and CPU Usage

AppFig has minimal memory and CPU overhead:
- Low memory footprint suitable for mobile devices
- Fast feature checks with intelligent caching
- Efficient rule evaluation

## Next Steps

- **[Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)** - All rule options
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your features
- **[Local Mode](#AppFig-docs%2FAdvanced%2Flocal-mode.md)** - Detailed local mode guide


---

<!-- SOURCE: AppFig-docs/SDK-Documentation/react-web-guide.md -->

# React/Web SDK Guide

Complete guide to integrating AppFig into your React or web application.

## Installation

> **SDK Download Repository:** [https://github.com/smartconfigui/AppFig-SDKs](https://github.com/smartconfigui/AppFig-SDKs)

**Requirements:** Modern browser with ES6+ support (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)

### Step 1: Download the SDK

Download the SDK files from:
```
https://github.com/smartconfigui/AppFig-SDKs/tree/main/react
```

You'll need:
- `AppFigReactSDK.js` (required) - Minified SDK bundle
- `AppFigReactSDK.d.ts` (required) - TypeScript type definitions

### Step 2: Add to Your Project

Copy both files to your project directory (e.g., `src/lib/appfig/`).

### Step 3: Import in Your Code

```typescript
import AppFig from './lib/appfig/AppFigReactSDK.js';
```

**TypeScript Support:**
If you copied `AppFigReactSDK.d.ts` to the same folder, TypeScript will automatically discover the type definitions. No additional configuration needed.

**Bundler Compatibility:**
The minified bundle works with all modern bundlers:
- ✅ Vite
- ✅ Webpack
- ✅ Rollup
- ✅ Parcel
- ✅ Create React App

**Benefits:**
- No npm installation required
- No build steps
- No dependency conflicts
- No version management hassles
- Works in any JavaScript/TypeScript project

## Quick Start

```typescript
import AppFig from './AppFigReactSDK';

// Initialize with your credentials (auto-refresh enabled by default)
await AppFig.init({
  companyId: 'your-company-id',
  tenantId: 'your-tenant-id',
  env: 'prod',                 // 'dev' or 'prod'
  apiKey: 'your-api-key',
  autoRefresh: true,           // Auto-refresh rules (default: true) ✨ NEW
  pollInterval: 3600000,       // 1 hour (default: 12 hours)
  debugMode: false,            // Enable debug logging (default: false) ✨ NEW
  sessionTimeoutMs: 1800000,   // 30 minutes (default: 30 min) ✨ NEW
  maxEvents: 5000,             // Max events to store (default: 5000)
  maxEventAgeDays: 7           // Max event age in days (default: 7)
});

// Log events
AppFig.logEvent('button_clicked', {
  button_id: 'upgrade',
  page: 'home'
});

// Check features
const welcomeMsg = AppFig.getConfig('welcome_message');
const showPremium = AppFig.getConfig('show_premium_banner');

if (showPremium === 'on') {
  showPremiumFeatures();
}
```

## When to Do What: Timing Guide

Understanding WHEN to call each SDK method is crucial for proper React/Web integration:

| Action | When to Call | Where to Place | Code Example |
|--------|-------------|----------------|--------------|
| **Initialize SDK** | Once at app startup | main.tsx or App.tsx top-level | `await AppFig.init(...)` |
| **Log Events** | Immediately after user action | Event handlers, onClick | `AppFig.logEvent('button_click')` |
| **Set User Properties** | Once at login, when value changes | Auth callback, state change | `AppFig.setUserProperties({ tier: 'premium' })` |
| **Evaluate Features** | When component mounts or renders | useEffect, component render | `AppFig.getConfig('feature_name')` |
| **Apply Features** | Immediately after evaluation | Same render cycle | `if (value) return <Component />` |
| **Refresh Rules** | On demand (auto-refresh handles this) | Manual: Settings page | `AppFig.refreshRules()` |

### Complete Lifecycle Example

```typescript
import React, { useEffect, useState } from 'react';
import AppFig from './AppFigReactSDK';

// 1. INITIALIZE: Once at app startup (main.tsx or App.tsx)
async function initializeApp() {
    await AppFig.init({
        companyId: 'your-company-id',
        tenantId: 'your-tenant-id',
        env: 'prod',
        apiKey: 'your-api-key'
    });

    // 2. TRACK: Log app open
    AppFig.logEvent('app_open');
}

// Call initialization
initializeApp();

function App() {
    const [showWelcome, setShowWelcome] = useState(false);
    const [playerLives, setPlayerLives] = useState(3);

    useEffect(() => {
        // 3. EVALUATE: Check features when component mounts
        checkWelcomeBonus();
        configureGameSettings();
    }, []);

    const checkWelcomeBonus = () => {
        // Evaluate right before using
        const bonusValue = AppFig.getConfig('welcome_bonus');

        // 4. APPLY: Update state immediately
        if (bonusValue === 'on') {
            setShowWelcome(true);
        }
    };

    const configureGameSettings = () => {
        // Evaluate with fallback
        const maxLives = parseInt(AppFig.getConfig('max_lives') || '3');

        // Apply to state
        setPlayerLives(maxLives);
    };

    const handleLevelComplete = () => {
        // TRACK: Log immediately after action
        AppFig.logEvent('level_complete', {
            level: currentLevel.toString()
        });

        // Re-evaluate features that depend on level
        checkNewUnlocks();
    };

    const handleSubscriptionPurchase = (tier: string) => {
        // Update property when it changes
        AppFig.setUserProperties({ subscription_tier: tier });

        // Re-evaluate premium features
        updatePremiumFeatures();
    };

    return (
        <div>
            {showWelcome && <WelcomeBonusModal />}
            <div>Lives: {playerLives}</div>
            <button onClick={handleLevelComplete}>Complete Level</button>
        </div>
    );
}
```

### Timing Best Practices

#### Initialize Once at Startup

```typescript
// ✅ CORRECT: Initialize in main.tsx before rendering
import AppFig from './AppFigReactSDK';

await AppFig.init({
    companyId: '...',
    tenantId: '...',
    env: 'prod',
    apiKey: '...'
});

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);

// ❌ WRONG: Don't initialize in every component
function MyComponent() {
    useEffect(() => {
        AppFig.init({...});  // Bad! SDK already initialized
    }, []);
}
```

#### Log Events Immediately

```typescript
// ✅ CORRECT: Log right when action happens
const handlePurchaseClick = () => {
    AppFig.logEvent('purchase_initiated', {
        item_id: 'premium_plan'
    });
    showPurchaseDialog();
};

// ❌ WRONG: Don't batch events
const pendingEvents: string[] = [];

const handleClick = () => {
    pendingEvents.push('button_clicked');  // Bad!
};
```

#### Cache Feature Values with State

```typescript
// ✅ CORRECT: Check once and cache in state
function GameComponent() {
    const [hasDoubleXP, setHasDoubleXP] = useState(false);

    useEffect(() => {
        const doubleXP = AppFig.getConfig('double_xp') === 'on';
        setHasDoubleXP(doubleXP);
    }, []);

    const calculateXP = (baseXP: number) => {
        return hasDoubleXP ? baseXP * 2 : baseXP;
    };

    return <div>XP: {calculateXP(100)}</div>;
}

// ❌ WRONG: Don't check on every render
function BadComponent() {
    // This checks on every single render - very inefficient!
    const hasDoubleXP = AppFig.getConfig('double_xp') === 'on';

    return <div>Has XP: {hasDoubleXP}</div>;
}
```

#### Evaluate in useEffect, Not Render

```typescript
// ✅ CORRECT: Evaluate in useEffect
function WelcomeScreen() {
    const [showBonus, setShowBonus] = useState(false);

    useEffect(() => {
        const bonusEnabled = AppFig.getConfig('welcome_bonus') === 'on';
        setShowBonus(bonusEnabled);
    }, []);

    return (
        <div>
            {showBonus && <BonusModal />}
        </div>
    );
}

// ❌ WRONG: Don't evaluate in render without memoization
function BadWelcomeScreen() {
    // This evaluates on every render
    const showBonus = AppFig.getConfig('welcome_bonus') === 'on';

    return (
        <div>
            {showBonus && <BonusModal />}
        </div>
    );
}
```

---

## API Reference

### Initialization

```typescript
await AppFig.init({
  companyId: string,              // Required: Your company ID
  tenantId: string,               // Required: Your tenant/project ID
  env: 'dev' | 'prod',            // Required: Environment
  apiKey: string,                 // Required: Your API key
  autoRefresh?: boolean,          // Optional: Auto-refresh rules (default: true) ✨ NEW DEFAULT
  pollInterval?: number,          // Optional: Poll interval in ms (default: 43200000 = 12 hours)
  debugMode?: boolean,            // Optional: Enable debug logging (default: false) ✨ NEW
  sessionTimeoutMs?: number,      // Optional: Session timeout (default: 1800000 = 30 min) ✨ NEW
  maxEvents?: number,             // Optional: Max events to store (default: 5000, max: 100000)
  maxEventAgeDays?: number,       // Optional: Max event age in days (default: 7, max: 365)

  // Deprecated (use autoRefresh instead):
  autoUpdate?: boolean            // Deprecated: Use autoRefresh instead
})
```

**Parameters:**
- `companyId` (required) - Your company ID from AppFig dashboard
- `tenantId` (required) - Your tenant/project ID from AppFig dashboard
- `env` (required) - Environment: `'dev'` or `'prod'`
- `apiKey` (required) - Your API key from AppFig dashboard
- `autoRefresh` (optional) - Enable automatic rule refreshing (default: **true**) ⚠️ **Breaking Change**
- `pollInterval` (optional) - Auto-refresh interval in milliseconds (default: 43200000 = 12 hours, min: server-enforced)
- `debugMode` (optional) - Enable debug console logs (default: false) 🆕 **New in v2.0**
- `sessionTimeoutMs` (optional) - Session timeout in milliseconds (default: 1800000 = 30 min, range: 60000-7200000) 🆕 **New in v2.0**
- `maxEvents` (optional) - Maximum events to store locally (100-100000, default: 5000)
- `maxEventAgeDays` (optional) - Maximum age of events in days (1-365, default: 7)
- `autoUpdate` (deprecated) - Use `autoRefresh` instead (backward compatibility maintained)

**Examples:**

```typescript
// Basic initialization (auto-refresh enabled by default)
await AppFig.init({
  companyId: 'acme-games',
  tenantId: 'space-shooter',
  env: 'prod',
  apiKey: 'sk_live_abc123xyz'
});

// With custom settings
await AppFig.init({
  companyId: 'acme-games',
  tenantId: 'space-shooter',
  env: 'prod',
  apiKey: 'sk_live_abc123xyz',
  autoRefresh: true,          // Explicit (but this is already the default)
  pollInterval: 3600000,      // 1 hour instead of 12 hours
  debugMode: true,            // See what's happening
  sessionTimeoutMs: 900000,   // 15 minutes instead of 30
  maxEvents: 10000            // Store more events
});

// Disable auto-refresh (opt-out)
await AppFig.init({
  companyId: 'acme-games',
  tenantId: 'space-shooter',
  env: 'prod',
  apiKey: 'sk_live_abc123xyz',
  autoRefresh: false         // Manual refresh only
});
```

### Logging Events

```typescript
AppFig.logEvent(name: string, params?: Record<string, any>)
```

**Examples:**
```typescript
// Simple event
AppFig.logEvent('page_view');

// Event with parameters
AppFig.logEvent('purchase_completed', {
  item_id: 'premium_plan',
  price: 29.99,
  currency: 'USD'
});

// Button click
AppFig.logEvent('button_clicked', {
  button_id: 'cta',
  page: 'landing',
  variant: 'blue'
});
```

### Checking Features

```typescript
AppFig.getConfig(flagName: string): any
```

Returns the feature value, or `null` if no rules match.

**Examples:**
```typescript
// Get feature value
const welcomeMsg = AppFig.getConfig('welcome_message');
if (welcomeMsg) {
  setMessage(welcomeMsg);
}

// Boolean features
const showPremium = AppFig.getConfig('show_premium_banner');
if (showPremium === 'on') {
  renderPremiumBanner();
}

// Numeric features
const maxRetries = AppFig.getConfig('max_api_retries');
const retries = parseInt(maxRetries || '3', 10);

// Complex features
const pricingConfig = AppFig.getConfig('pricing_tiers');
// Returns: { basic: 9, pro: 29, enterprise: 99 }
```

### Setting Properties

```typescript
// User properties
AppFig.setUserProperties(props: Record<string, any>)

// Device properties
AppFig.setDeviceProperties(props: Record<string, any>)

// App version
AppFig.setAppVersion(version: string)
```

**Examples:**
```typescript
// Set user properties
AppFig.setUserProperties({
  subscription_tier: 'premium',
  account_age_days: 45,
  preferred_theme: 'dark'
});

// Set device properties
AppFig.setDeviceProperties({
  browser_name: 'Chrome',
  screen_width: 1920
});

// Set app version
AppFig.setAppVersion('2.1.0');
```

### Auto-Detected Properties

These are automatically set by the SDK on initialization:

```javascript
country          "US", "JP", "GB" (from CDN response headers)
platform         "macOS", "Windows", "Linux", "iOS", "Android"
language         "en-US", "ja", "es-ES" (from browser)
timezone         "America/Los_Angeles", "Asia/Tokyo" (from system)
os_version       "10.15.7", "11.0", "14.2" (from user agent)
device_brand     "Apple", "Microsoft", "Google"
device_model     "Chrome 120 on macOS", "Firefox 121 on Windows"
app_version      "1.0.0" (you set this with setAppVersion())
sdk_version      "2.0.0" (AppFig SDK version)
```

**Country Detection:**
- Primary: Extracted from CDN response `Country` header (Cloudflare Workers)
- Fallback: Static endpoint `rules-dev.appfig.com` with 5-second timeout
- No third-party services used (fully controlled by AppFig infrastructure)

### Manual Refresh

```typescript
await AppFig.refreshRules()
AppFig.stopAutoRefresh()
```

**Examples:**
```typescript
// Manually refresh rules
await AppFig.refreshRules();

// Stop auto-refresh
AppFig.stopAutoRefresh();
```

### Feature Reset (Recurring Triggers)

```typescript
AppFig.resetFeature(featureName: string)
AppFig.resetAllFeatures()
```

Reset a feature's cached value and force re-evaluation. Useful for implementing recurring triggers like "show popup every 3 events".

**Example:**
```typescript
// Check if feature is enabled
if (AppFig.getConfig("level_complete_popup")) {
  showPopup("You've completed 3 levels!");

  // Reset so it triggers again after next 3 events
  AppFig.resetFeature("level_complete_popup");
}

// With event logging
function onLevelComplete(level: number) {
  AppFig.logEvent("level_complete", { level: level.toString() });

  // Check for milestone reward
  if (AppFig.getConfig("milestone_reward")) {
    showReward();
    AppFig.resetFeature("milestone_reward");
  }
}
```

**Use Cases:**
- Show popup every N events (e.g., every 3 level completions)
- Recurring promotions after X purchases
- Periodic reward unlocks
- Time-based feature re-triggers

### Debugging

```typescript
AppFig.getDebugInfo()
AppFig.getEventHistory()
```

**Example:**
```typescript
const debug = AppFig.getDebugInfo();
console.log('Rules:', debug.rules);
console.log('Events:', debug.events);
console.log('User properties:', debug.userProperties);
console.log('Device properties:', debug.deviceProperties);
console.log('Cached results:', debug.cachedResults);

const events = AppFig.getEventHistory();
console.log(`Logged ${events.length} events`);
```

## React Integration

### Basic Setup

```typescript
import { useEffect, useState } from 'react';
import AppFig from './AppFigReactSDK';

function App() {
  const [isInitialized, setIsInitialized] = useState(false);
  const [welcomeMessage, setWelcomeMessage] = useState('');

  useEffect(() => {
    async function init() {
      await AppFig.init({
        companyId: 'acme-games',
        tenantId: 'my-app',
        env: 'prod',
        apiKey: 'sk_live_abc123'
      });
      setIsInitialized(true);

      // Get initial feature values
      const msg = AppFig.getConfig('welcome_message');
      setWelcomeMessage(msg || 'Welcome!');
    }
    init();
  }, []);

  if (!isInitialized) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{welcomeMessage}</h1>
    </div>
  );
}
```

### Custom Hook

```typescript
import { useState, useEffect } from 'react';
import AppFig from './AppFigReactSDK';

export function useFeature(featureName: string) {
  const [value, setValue] = useState<any>(null);

  useEffect(() => {
    const val = AppFig.getConfig(featureName);
    setValue(val);
  }, [featureName]);

  return value;
}

// Usage
function MyComponent() {
  const showBanner = useFeature('premium_banner');
  const maxRetries = useFeature('api_max_retries');

  return (
    <div>
      {showBanner === 'on' && <PremiumBanner />}
      <ApiClient maxRetries={parseInt(maxRetries || '3', 10)} />
    </div>
  );
}
```

### Context Provider

```typescript
import React, { createContext, useContext, useEffect, useState } from 'react';
import AppFig from './AppFigReactSDK';

const AppFigContext = createContext<{
  isInitialized: boolean;
  getFeature: (name: string) => any;
  logEvent: (name: string, params?: any) => void;
}>({
  isInitialized: false,
  getFeature: () => null,
  logEvent: () => {}
});

export function AppFigProvider({
  children,
  companyId,
  tenantId,
  env,
  apiKey
}: {
  children: React.ReactNode;
  companyId: string;
  tenantId: string;
  env: 'dev' | 'prod';
  apiKey: string;
}) {
  const [isInitialized, setIsInitialized] = useState(false);

  useEffect(() => {
    async function init() {
      await AppFig.init({ companyId, tenantId, env, apiKey });
      setIsInitialized(true);
    }
    init();
  }, [companyId, tenantId, env, apiKey]);

  const value = {
    isInitialized,
    getFeature: (name: string) => AppFig.getConfig(name),
    logEvent: (name: string, params?: any) => AppFig.logEvent(name, params)
  };

  return (
    <AppFigContext.Provider value={value}>
      {children}
    </AppFigContext.Provider>
  );
}

export function useAppFig() {
  return useContext(AppFigContext);
}

// Usage
function App() {
  return (
    <AppFigProvider
      companyId="acme-games"
      tenantId="my-app"
      env="prod"
      apiKey="sk_live_abc123"
    >
      <MyComponent />
    </AppFigProvider>
  );
}

function MyComponent() {
  const { isInitialized, getFeature, logEvent } = useAppFig();

  if (!isInitialized) {
    return <div>Loading...</div>;
  }

  const showBanner = getFeature('premium_banner');

  return (
    <div>
      {showBanner === 'on' && <PremiumBanner />}
      <button onClick={() => logEvent('button_clicked', { id: 'cta' })}>
        Upgrade Now
      </button>
    </div>
  );
}
```

## Automatic Tracking

The SDK automatically tracks these events:

```typescript
'first_open'     // First time user visits (persisted in localStorage)
'session_start'  // Page becomes visible
'session_end'    // Page hidden, tab/window closed, or `sessionTimeoutMs` of inactivity (default: 30 min, range: 1 min - 2 hr)
'screen_view'    // URL changes (single-page apps)
```

### Screen View Tracking

Automatically tracks navigation in single-page apps:

```typescript
// Automatically logged when URL changes
// Works with:
// - History API (pushState, replaceState)
// - Hash changes (#/page)
// - Browser back/forward buttons

// Each screen view includes:
AppFig.logEvent('screen_view', {
  screen_name: 'dashboard',     // From URL path or hash
  previous_screen: 'home'       // Previous screen
});
```

## Debug Mode

Enable debug logging to see detailed SDK activity:

```typescript
await AppFig.init({
  companyId: 'acme-games',
  tenantId: 'my-app',
  env: 'prod',
  apiKey: 'sk_live_abc123',
  debugMode: true  // ← Enable debug logs
});

// Console output:
// [AppFig] 📱 Detected platform: macOS
// [AppFig] 🌍 Detected language: en-US
// [AppFig] 🔧 Device properties set: country, platform, language
// [AppFig] ✅ Loaded rules from CDN - Feature count: 5
// [AppFig] 🔍 getConfig called for: welcome_message
// [AppFig] 🔍 Returning CACHED result for welcome_message: "Hello!"
```

## Best Practices

### 1. Initialize Once

Initialize at app entry point:

```typescript
// ✅ Good: Initialize in root component
function App() {
  useEffect(() => {
    AppFig.init({
      companyId: 'acme-games',
      tenantId: 'my-app',
      env: 'prod',
      apiKey: 'sk_live_abc123'
    });
  }, []);
  // ...
}

// ❌ Bad: Initializing in multiple places
function Header() {
  useEffect(() => {
    AppFig.init({ ... });  // Don't repeat!
  }, []);
}
```

### 2. Cache Feature Values in State

```typescript
// ✅ Good: Check once, store in state
function Component() {
  const [showBanner, setShowBanner] = useState(false);

  useEffect(() => {
    const value = AppFig.getConfig('show_banner');
    setShowBanner(value === 'on');
  }, []);

  return showBanner ? <Banner /> : null;
}

// ❌ Bad: Checking on every render
function Component() {
  const showBanner = AppFig.getConfig('show_banner') === 'on';
  return showBanner ? <Banner /> : null;
}
```

### 3. Handle Null Values

```typescript
// ✅ Good: Provide fallback
const maxRetries = parseInt(
  AppFig.getConfig('max_retries') || '3',
  10
);

// ❌ Bad: No fallback
const maxRetries = parseInt(AppFig.getConfig('max_retries'), 10);
// NaN if feature returns null!
```

### 4. Log Events Generously

```typescript
function handlePurchase(item: string, price: number) {
  // Log detailed events
  AppFig.logEvent('purchase_started', {
    item_id: item,
    price: price.toString(),
    currency: 'USD',
    page: window.location.pathname
  });

  // ... process purchase ...

  AppFig.logEvent('purchase_completed', {
    item_id: item,
    price: price.toString(),
    payment_method: 'stripe'
  });
}
```

## TypeScript Types

```typescript
import type {
  AppFigRule,
  AppFigInitParams,
  AppFigEvent,
  Condition
} from './AppFigReactSDK';

// Use types in your code
const params: AppFigInitParams = {
  companyId: 'acme-games',
  tenantId: 'my-app',
  env: 'prod',
  apiKey: 'sk_live_abc123',
  debugMode: true
};
```

## Performance Tips

### Caching

Features are cached after first evaluation for optimal performance:

```typescript
// First call: Evaluates rules
const msg1 = AppFig.getConfig('welcome_message');

// Second call: Returns cached value (nearly instant)
const msg2 = AppFig.getConfig('welcome_message');
```

Cache is invalidated when:
- New events are logged
- User/device properties change
- Rules are refreshed

### Bundle Size

The SDK has a small footprint suitable for web applications, with minimal impact on your application's bundle size after minification and compression.

## Troubleshooting

### Module not found or import errors

**Problem:** Bundler can't find the SDK file

**Solutions:**
1. Verify `AppFigReactSDK.js` is in the correct location
2. Check import path is correct (use relative path like `'./lib/appfig/AppFigReactSDK.js'`)
3. For Webpack/CRA: Ensure path starts with `./` or `../`
4. For Vite: Import path should work as-is

### TypeScript errors: "Could not find a declaration file"

**Problem:** TypeScript shows type errors

**Solutions:**
1. Verify `AppFigReactSDK.d.ts` is in the same folder as `AppFigReactSDK.js`
2. If using a different location, add `/// <reference path="./path/to/AppFigReactSDK.d.ts" />` at the top of your file
3. Or use `// @ts-ignore` above the import if types aren't needed

### Rules not loading

**Problem:** `Failed to load rules from CDN`

**Solutions:**
1. Verify API key is correct
2. Check browser console for network errors
3. Ensure company ID and tenant ID are correct
4. Verify environment ('dev' or 'prod') matches your dashboard setup

### Features always return null

**Problem:** No rules match

**Solutions:**
1. Verify feature name spelling (case-sensitive)
2. Check rules are loaded: `AppFig.getDebugInfo().rules`
3. Log relevant events/properties before checking feature
4. Enable debug mode to see rule evaluation

### Auto-refresh not working

**Problem:** Changes in dashboard not reflected

**Solutions:**
1. Verify `autoRefresh: true` in initialization (the older `autoUpdate` field still works but is deprecated — use `autoRefresh`)
2. Wait for poll interval (default 12 hours)
3. Check browser console for refresh logs
4. Call `AppFig.refreshRules()` manually

## Next Steps

- **[Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)** - All rule options
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your features
- **[Local Mode](#AppFig-docs%2FAdvanced%2Flocal-mode.md)** - Offline development mode


---

<!-- SOURCE: AppFig-docs/SDK-Documentation/unity-integration.md -->

# Unity SDK Integration Guide

Complete guide to integrating AppFig into your Unity project.

## Installation

> **SDK Download Repository:** [https://github.com/smartconfigui/AppFig-SDKs](https://github.com/smartconfigui/AppFig-SDKs)

**Requirements:** Unity 2019.4+ (supports all platforms: iOS, Android, Windows, macOS, Linux, WebGL)

### Step 1: Download the SDK

Download `AppFigUnitySDK.dll` from:
```
https://github.com/smartconfigui/AppFig-SDKs/tree/main/unity
```

### Step 2: Add to Unity Project

1. In Unity, create the folder structure: `Assets/Plugins/AppFig/`
2. Drag `AppFigUnitySDK.dll` into the `Assets/Plugins/AppFig/` folder
3. Unity will automatically detect the assembly

**That's it!** No additional configuration needed.

### What's Included

The single DLL contains all SDK functionality:
- **Core SDK** - `AppFig` class with all feature flag and event methods
- **AppFigHelper** - Optional- MonoBehaviour wrapper for Inspector-based setup
- **AppFigApplier** - Optional- No-code component for applying feature values to GameObjects
- **AppFigABTester** - Optional- Visual A/B testing component

No meta files, no dependencies, no build steps required.

## Quick Start

### Option 1: Cloud Mode (Paid Plans)

Initialize with your company credentials:

```csharp
using UnityEngine;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // Initialize SDK
        AppFig.Init(
            companyId: "your-company-id",
            tenantId: "your-tenant-id",
            env: "prod",              // "dev" or "prod"
            apiKey: "your-api-key"
        );

        // Log events
        AppFig.LogEvent("app_open");

        // Check features
        if (AppFig.IsFeatureEnabled("double_xp")) {
            ApplyDoubleXP();
        }

        string maxLives = AppFig.GetFeatureValue("max_lives");
        int lives = int.Parse(maxLives);
    }
}
```

### Option 2: Local Mode (Free Plan)

Use a JSON file bundled with your app:

```csharp
using UnityEngine;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // Initialize in local mode (loads from Resources/appfig_rules.json)
        AppFig.InitLocal();

        // Use features exactly the same way
        if (AppFig.IsFeatureEnabled("tutorial_mode")) {
            StartTutorial();
        }
    }
}
```

## When to Do What: Timing Guide

Understanding WHEN to call each SDK method is crucial for proper implementation. Follow this guide to ensure optimal integration:

| Action | When to Call | Where to Place | Code Example |
|--------|-------------|----------------|--------------|
| **Initialize SDK** | Once at app startup, before any SDK calls | `GameManager.Start()` or `Awake()` | `AppFig.Init(...)` |
| **Log Events** | Immediately after user action | Event handlers, button callbacks | `AppFig.LogEvent("level_complete")` |
| **Set User Properties** | Once at login, when value changes | Auth callback, state change | `AppFig.SetUserProperty("tier", "premium")` |
| **Set Device Properties** | Once at startup (usually auto-detected) | `Start()` method | `AppFig.SetAppVersion("1.0.0")` |
| **Evaluate Features** | Right before you need the value | Decision points, scene load | `AppFig.GetFeatureValue("max_lives")` |
| **Apply Features** | Immediately after evaluation | Same method as evaluation | `if (value) { ShowUI(); }` |
| **Refresh Rules** | On demand or periodically (auto-refresh handles this) | Manual: Settings screen | `AppFig.RefreshRules()` |

### Detailed Timing Scenarios

#### Initialization Timing

```csharp
public class GameManager : MonoBehaviour
{
    void Awake()
    {
        // ✅ CORRECT: Initialize once at startup
        // This ensures SDK is ready before any other code runs
        AppFig.Init("company-id", "tenant-id", "prod", "api-key");
    }

    void Start()
    {
        // ✅ Also acceptable: Initialize in Start()
        // Just ensure it happens before any SDK calls
        AppFig.InitLocal();
    }
}

// ❌ WRONG: Don't initialize in every scene
public class LevelManager : MonoBehaviour
{
    void Start()
    {
        AppFig.Init(...); // Bad! SDK already initialized
    }
}

// ❌ WRONG: Don't initialize multiple times
void Update()
{
    AppFig.Init(...); // Bad! Only initialize once
}
```

#### Event Logging Timing

```csharp
// ✅ CORRECT: Log immediately after action
public void OnLevelComplete()
{
    SaveProgress();
    ShowCompletionUI();

    // Log right when it happens
    AppFig.LogEvent("level_complete", new Dictionary<string, string> {
        { "level_number", currentLevel.ToString() },
        { "time_seconds", elapsedTime.ToString() }
    });
}

// ✅ CORRECT: Log on button click
public void OnPurchaseButtonClicked()
{
    // Log the intention immediately
    AppFig.LogEvent("purchase_initiated");

    ShowPurchaseDialog();
}

// ❌ WRONG: Don't batch events
private List<string> pendingEvents = new List<string>();

void OnLevelComplete()
{
    pendingEvents.Add("level_complete"); // Bad! Log immediately
}

void OnApplicationQuit()
{
    // Bad! Events should be logged as they happen
    foreach (var evt in pendingEvents) {
        AppFig.LogEvent(evt);
    }
}

// ❌ WRONG: Don't log in Update()
void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        // Bad! Log in the actual event handler
        AppFig.LogEvent("space_pressed");
    }
}
```

#### Property Setting Timing

```csharp
// ✅ CORRECT: Set once when value is established
public void OnLoginSuccess(string userId, string tier)
{
    // Set properties right after authentication
    AppFig.SetUserProperty("user_id", userId);
    AppFig.SetUserProperty("subscription_tier", tier);
    AppFig.SetUserProperty("login_date", DateTime.Now.ToString());
}

// ✅ CORRECT: Update when value changes
public void OnSubscriptionUpgrade(string newTier)
{
    // Update property when it changes
    AppFig.SetUserProperty("subscription_tier", newTier);

    // Re-evaluate features that depend on tier
    UpdateUIBasedOnTier();
}

// ❌ WRONG: Don't set repeatedly with same value
void Update()
{
    // Bad! Set once, not every frame
    AppFig.SetUserProperty("user_id", currentUserId);
}

// ❌ WRONG: Don't set before initialization
void Awake()
{
    AppFig.SetUserProperty("level", "1"); // Bad! Init first

    AppFig.Init(...); // Too late
}
```

#### Feature Evaluation Timing

```csharp
// ✅ CORRECT: Evaluate when you need the value
public void LoadLevel(int levelNumber)
{
    // Check feature right before using it
    string difficultyValue = AppFig.GetFeatureValue("difficulty_multiplier");
    float difficulty = float.Parse(difficultyValue ?? "1.0");

    // Apply immediately
    ApplyDifficultySettings(difficulty);
    LoadScene(levelNumber);
}

// ✅ CORRECT: Cache for performance
private bool hasDoubleXP = false;

void Start()
{
    // Check once and cache the result
    hasDoubleXP = AppFig.IsFeatureEnabled("double_xp");
}

void CalculateXP(int baseXP)
{
    // Use cached value in hot path
    return hasDoubleXP ? baseXP * 2 : baseXP;
}

// ❌ WRONG: Don't check every frame
void Update()
{
    // Bad! Very inefficient
    if (AppFig.IsFeatureEnabled("double_xp"))
    {
        ApplyDoubleXP();
    }
}

// ❌ WRONG: Don't check before logging relevant events
void Start()
{
    // Bad! Feature evaluates to null because no events logged yet
    if (AppFig.IsFeatureEnabled("veteran_bonus")) { }

    // Should log events first
    AppFig.LogEvent("app_open");

    // Now this works
    if (AppFig.IsFeatureEnabled("veteran_bonus")) { }
}
```

#### Feature Application Timing

```csharp
// ✅ CORRECT: Apply immediately after evaluation
void Start()
{
    // Evaluate
    bool showBonus = AppFig.IsFeatureEnabled("welcome_bonus");

    // Apply right away with null handling
    if (showBonus)
    {
        welcomeBonusPanel.SetActive(true);
        GrantCoins(500);
    }
    else
    {
        welcomeBonusPanel.SetActive(false);
    }
}

// ✅ CORRECT: Apply configuration values
void LoadGameSettings()
{
    // Evaluate with fallback
    string maxLivesStr = AppFig.GetFeatureValue("max_lives");
    int maxLives = int.Parse(maxLivesStr ?? "3");

    // Apply to game state
    playerMaxLives = maxLives;
    UpdateUI();
}

// ❌ WRONG: Don't apply in different frame/context
private string featureValue;

void Start()
{
    featureValue = AppFig.GetFeatureValue("theme");
}

void Update()
{
    // Bad! Apply immediately after evaluation
    if (featureValue == "dark")
    {
        ApplyDarkTheme();
    }
}

// ❌ WRONG: Don't forget null handling
void Start()
{
    string maxLives = AppFig.GetFeatureValue("max_lives");

    // Bad! What if maxLives is null?
    int lives = int.Parse(maxLives); // NullReferenceException!

    // Should be:
    int lives = int.Parse(maxLives ?? "3");
}
```

### Complete Lifecycle Example

Here's a complete example showing proper timing for all SDK operations:

```csharp
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;

public class GameManager : MonoBehaviour
{
    public GameObject welcomePanel;
    public Text livesText;
    private int playerLives = 3;

    // 1. INITIALIZE: Once at startup
    void Start()
    {
        AppFig.InitLocal();

        // 2. SET PROPERTIES: Once when known (device props are auto-detected)
        AppFig.SetAppVersion("1.0.0");

        // 3. TRACK: Log events as they happen
        AppFig.LogEvent("app_open");

        // 4. EVALUATE: When you need values
        CheckWelcomeBonus();
        ConfigureGameSettings();
    }

    void CheckWelcomeBonus()
    {
        // Evaluate right before using
        bool showWelcome = AppFig.IsFeatureEnabled("welcome_bonus");

        // Apply immediately
        if (showWelcome)
        {
            welcomePanel.SetActive(true);
        }
    }

    void ConfigureGameSettings()
    {
        // Evaluate with fallback
        string maxLivesValue = AppFig.GetFeatureValue("max_lives");
        playerLives = int.Parse(maxLivesValue ?? "3");

        // Apply to UI
        livesText.text = $"Lives: {playerLives}";
    }

    public void OnLevelComplete()
    {
        // Track immediately after action
        AppFig.LogEvent("level_complete", new Dictionary<string, string> {
            { "level", currentLevel.ToString() }
        });

        // Re-evaluate features that depend on level completion
        CheckNewUnlocks();
    }

    public void OnSubscriptionPurchased(string tier)
    {
        // Update property when it changes
        AppFig.SetUserProperty("subscription_tier", tier);

        // Re-evaluate features that depend on subscription
        UpdatePremiumFeatures();
    }
}
```

---

## API Reference

### Initialization

#### Cloud Mode
```csharp
AppFig.Init(
  string companyId,                    // Required: Your company ID
  string tenantId,                     // Required: Your tenant/project ID
  string env,                          // Required: "dev" or "prod"
  string apiKey,                       // Required: Your API key
  bool autoRefresh = true,             // Optional: Enable auto-refresh (default: true) ✨ NEW DEFAULT
  long pollInterval = 43200000,        // Optional: 12 hours (default)
  bool debugMode = false,              // Optional: Enable debug logging (default: false) ✨ NEW
  long sessionTimeoutMs = 1800000,     // Optional: 30 minutes (default: 30 min) ✨ NEW
  int maxEvents = 5000,                // Optional: Max events (default: 5000, max: 100000)
  int maxEventAgeDays = 7              // Optional: Max age in days (default: 7, max: 365)
)
```

**Parameters:**
- `companyId` (required) - Your company ID from AppFig dashboard
- `tenantId` (required) - Your tenant/project ID from AppFig dashboard
- `env` (required) - Environment: `"dev"` or `"prod"`
- `apiKey` (required) - Your API key from AppFig dashboard
- `autoRefresh` (optional) - Enable automatic background rule updates (default: **true**) ⚠️ **Breaking Change**
- `pollInterval` (optional) - Auto-refresh interval in milliseconds (default: 43200000 = 12 hours, min: server-enforced)
- `debugMode` (optional) - Enable debug logging to Unity console (default: false) 🆕 **New in v2.0**
- `sessionTimeoutMs` (optional) - Session timeout in milliseconds (default: 1800000 = 30 min, range: 60000-7200000) 🆕 **New in v2.0**
- `maxEvents` (optional) - Maximum events to store in PlayerPrefs (100-100000, default: 5000)
- `maxEventAgeDays` (optional) - Maximum age of events in days (1-365, default: 7)

**Examples:**

```csharp
// Basic initialization (auto-refresh enabled by default)
AppFig.Init("acme-games", "space-shooter", "prod", "sk_live_abc123");

// With custom poll interval (1 hour instead of 12 hours)
AppFig.Init(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123",
    autoRefresh: true,          // Explicit (but this is already the default)
    pollInterval: 3600000       // 1 hour
);

// With debug mode enabled (useful during development)
AppFig.Init(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "dev",                 // Use dev environment
    apiKey: "sk_test_abc123",
    autoRefresh: true,
    pollInterval: 600000,       // 10 minutes for faster testing
    debugMode: true             // See all SDK operations in console
);

// With all parameters customized
AppFig.Init(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123",
    autoRefresh: true,
    pollInterval: 3600000,
    debugMode: false,
    sessionTimeoutMs: 900000,   // 15 minutes instead of 30
    maxEvents: 10000,           // Store more events
    maxEventAgeDays: 30         // Keep events for 30 days
);

// Disable auto-refresh (opt-out, manual refresh only)
AppFig.Init(
    companyId: "acme-games",
    tenantId: "space-shooter",
    env: "prod",
    apiKey: "sk_live_abc123",
    autoRefresh: false          // Must call RefreshRules() manually
);
```

#### Local Mode
```csharp
AppFig.InitLocal()
AppFig.InitLocal(string resourcePath)
```

**Parameters:**
- `resourcePath` (optional) - Path to JSON file in Resources folder (default: `"appfig_rules"`)

**Example:**
```csharp
// Loads from Resources/appfig_rules.json
AppFig.InitLocal();

// Loads from Resources/my_custom_rules.json
AppFig.InitLocal("my_custom_rules");
```

### Logging Events

```csharp
AppFig.LogEvent(string eventName)
AppFig.LogEvent(string eventName, Dictionary<string, string> parameters)
```

**Examples:**
```csharp
// Simple event
AppFig.LogEvent("level_complete");

// Event with parameters
AppFig.LogEvent("purchase_made", new Dictionary<string, string> {
    { "item_id", "sword_legendary" },
    { "price", "9.99" },
    { "currency", "USD" }
});

// Screen view (auto-tracked by SDK too)
AppFig.LogScreenView("main_menu", "settings");
```

### Checking Features

```csharp
bool AppFig.IsFeatureEnabled(string featureName)
string AppFig.GetFeatureValue(string featureName)
```

**Examples:**
```csharp
// Boolean check (returns true if value == "on")
if (AppFig.IsFeatureEnabled("new_ui")) {
    LoadNewUI();
} else {
    LoadOldUI();
}

// Get actual value
string maxLives = AppFig.GetFeatureValue("max_lives");
if (maxLives != null) {
    int lives = int.Parse(maxLives);
    playerLives = lives;
}

// Handle null (no matching rules)
string bonus = AppFig.GetFeatureValue("veteran_bonus");
if (bonus == null) {
    Debug.Log("No bonus active");
} else {
    ApplyBonus(bonus); // "2x", "1.5x", etc.
}
```

### Setting Properties

```csharp
// User properties
AppFig.SetUserProperty(string key, string value)
AppFig.RemoveUserProperty(string key)

// Device properties
AppFig.SetDeviceProperty(string key, string value)
AppFig.RemoveDeviceProperty(string key)

// App version (convenience method)
AppFig.SetAppVersion(string version)
```

**Examples:**
```csharp
// User properties
AppFig.SetUserProperty("subscription_tier", "premium");
AppFig.SetUserProperty("account_age_days", "30");
AppFig.SetUserProperty("player_level", "15");

// Device properties (most are auto-detected, but you can add custom ones)
AppFig.SetAppVersion("1.2.3");
AppFig.SetDeviceProperty("graphics_quality", "high");

// Remove properties
AppFig.RemoveUserProperty("subscription_tier");
```

### Auto-Detected Device Properties

These are automatically set by the SDK on initialization:

```
platform          "iOS", "Android", "Windows", "macOS", "Linux", etc.
os_version        "17.0", "13", "11.0"
language          "English", "Japanese", "Spanish"
timezone          "America/Los_Angeles", "Asia/Tokyo"
country           "US", "JP", "GB" (requires internet on first run)
device_brand      "Apple", "Samsung", "Google", "Huawei"
device_model      "iPhone 15", "Galaxy S23", "Pixel 8"
```

Use these in your rules without any setup!

### Session Tracking

Automatically tracked:

```csharp
// These events are logged automatically:
"first_open"     // First time app opens (persisted in PlayerPrefs)
"session_start"  // App gains focus
"session_end"    // App loses focus, or `sessionTimeoutMs` of inactivity (default: 30 min, range: 1 min - 2 hr)
"screen_view"    // When you call LogScreenView()
```

### Manual Refresh

```csharp
AppFig.RefreshRules()  // Manually reload rules from CDN
```

**Example:**
```csharp
// Manually refresh rules (useful for testing rule changes)
AppFig.RefreshRules();
```

**Note:** `SetAutoRefresh()` and `SetPollInterval()` are deprecated. Use the `autoRefresh` and `pollInterval` parameters in `Init()` instead.

### Feature Reset (Recurring Triggers)

```csharp
AppFig.ResetFeature(string featureName)
AppFig.ResetAllFeatures()
```

Reset a feature's cached value and force re-evaluation. Useful for implementing recurring triggers like "show popup every 3 events".

**Example:**
```csharp
// Check if feature is enabled
if (AppFig.IsFeatureEnabled("level_complete_popup")) {
    ShowPopup("You've completed 3 levels!");

    // Reset so it triggers again after next 3 events
    AppFig.ResetFeature("level_complete_popup");
}

// With event logging
void OnLevelComplete(int level) {
    AppFig.LogEvent("level_complete", new Dictionary<string, string> {
        { "level", level.ToString() }
    });

    // Check for milestone reward
    if (AppFig.IsFeatureEnabled("milestone_reward")) {
        ShowReward();
        AppFig.ResetFeature("milestone_reward");
    }
}
```

**Use Cases:**
- Show popup every N events (e.g., every 3 level completions)
- Recurring promotions after X purchases
- Periodic reward unlocks
- Time-based feature re-triggers

### Debugging

```csharp
AppFig.GetDebugInfo()  // Returns dict with rules, events, properties, cache
```

**Example:**
```csharp
var debug = AppFig.GetDebugInfo();
Debug.Log($"Rules loaded: {debug.rules.Count}");
Debug.Log($"Events logged: {debug.events.Count}");
Debug.Log($"User properties: {debug.userProperties.Count}");
Debug.Log($"Cached features: {debug.cachedResults.Count}");
```

## Using AppFigHelper (Optional)

**Note:** `AppFigHelper`, `AppFigApplier`, and `AppFigABTester` are Unity-only no-code convenience components. There is no equivalent on Android, iOS, or React/Web — those platforms only expose the core `AppFig` API.

`AppFigHelper` is a MonoBehaviour that makes initialization easier with Inspector setup.

### Setup

1. Create an empty GameObject in your first scene
2. Add `AppFigHelper` component
3. Configure in Inspector:

**For Cloud Mode:**
- Company ID: `your-company-id`
- Tenant ID: `your-tenant-id`
- Environment: `prod` or `dev`
- API Key: `your-api-key`
- Use Local Mode: ☐ (unchecked)

**For Local Mode:**
- Use Local Mode: ☑ (checked)
- Local Rules Path: `appfig_rules` (or custom path)
- Enable Debug Logs: ☑ (optional)

### Usage

```csharp
// Access anywhere via singleton
AppFigHelper.Instance.LogEvent("button_clicked");

string value = AppFigHelper.Instance.GetFeatureValue("max_lives");

if (AppFigHelper.Instance.IsFeatureEnabled("double_xp")) {
    ApplyDoubleXP();
}
```

### No-Code Event Logging with Unity UI

You can connect AppFigHelper methods directly to Unity UI components without writing any code!

#### Button onClick Events

**Setup:**
1. Select your Button in the scene
2. In the Inspector, find the **OnClick()** section
3. Click the **+** button to add a new event
4. Drag your AppFigHelper GameObject into the object field
5. Select **AppFigHelper** → **LogEvent(string)**
6. Enter your event name (e.g., "button_clicked")

**Example:**
```
GameObject: PlayButton
Component: Button
  OnClick():
    - AppFigHelper.LogEvent("play_button_clicked")

GameObject: SettingsButton
Component: Button
  OnClick():
    - AppFigHelper.LogEvent("settings_opened")
```

#### Tracking Multiple Interactions

You can add multiple AppFigHelper calls to a single button:

```
GameObject: PurchaseButton
Component: Button
  OnClick():
    - AppFigHelper.LogEvent("purchase_initiated")
    - AppFigHelper.SetUserProperty("last_purchase_attempt", "now")
    - PurchaseManager.ProcessPurchase()  // Your custom method
```

#### Dropdown onChange Events

Track dropdown selections:

```
GameObject: DifficultyDropdown
Component: Dropdown
  OnValueChanged():
    - AppFigHelper.LogEvent("difficulty_changed")
```

#### Toggle onValueChanged Events

Track toggle switches:

```
GameObject: SoundToggle
Component: Toggle
  OnValueChanged():
    - AppFigHelper.LogEvent("sound_toggled")
```

#### Slider onValueChanged Events

Track slider adjustments:

```
GameObject: VolumeSlider
Component: Slider
  OnValueChanged():
    - AppFigHelper.LogEvent("volume_adjusted")
```

**Benefits:**
- Zero code required for basic event logging
- Quick prototyping and testing
- Perfect for designers and non-programmers
- Can be combined with custom scripts

**Note:** For events with parameters, you'll still need to write a simple wrapper method. See [Logging Events](#logging-events) above.

## Visual Components (No Code!)

### AppFigApplier

Apply feature values to GameObjects without writing code.

**Use cases:**
- Show/hide objects based on features
- Update text with feature values
- Set numeric values (light intensity, volume, speed)

**Setup:**
1. Add `AppFigApplier` component to any GameObject
2. Set Feature Name (e.g., "show_bonus_ui")
3. Choose Apply Type:
   - **Enable/Disable**: Show/hide the GameObject
   - **Text Content**: Update Text/TextMeshPro component
   - **Numeric Value**: Set float value on component (volume, intensity, etc.)

**Example:**
```
GameObject: BonusPanel
Component: AppFigApplier
  - Feature Name: "bonus_panel"
  - Apply Type: Enable/Disable

Rule: IF user completed 10+ levels THEN "bonus_panel" = "on"
Result: BonusPanel appears after 10 level completions
```

### AppFigABTester

A/B test visual elements with unlimited variants.

**Use cases:**
- Test different button colors
- Swap sprites/materials
- Try different prefabs
- Experiment with audio

**Setup:**
1. Add `AppFigABTester` component to any GameObject
2. Set Feature Name (e.g., "button_color")
3. Choose Component Type (Sprite, Material, Color, Prefab, AudioClip)
4. Define variants with feature values

**Example:**
```
GameObject: UpgradeButton
Component: AppFigABTester
  - Feature Name: "button_color"
  - Component Type: Color (Image)
  - Variants:
    * Feature Value: "red" → Color: #FF0000
    * Feature Value: "blue" → Color: #0000FF
    * Feature Value: "green" → Color: #00FF00

Rule: IF user from US THEN "button_color" = "red"
Rule: IF user from EU THEN "button_color" = "blue"
```

## Best Practices

### 1. Initialize Early

Initialize in your first scene's `Start()` or `Awake()`:

```csharp
void Awake()
{
    AppFig.Init("company", "tenant", "prod", "api-key");
}
```

### 2. Cache Feature Values

Don't check the same feature repeatedly:

```csharp
// ❌ Bad: Checks every frame
void Update() {
    if (AppFig.IsFeatureEnabled("double_xp")) {
        // ...
    }
}

// ✅ Good: Check once, store result
private bool hasDoubleXP;

void Start() {
    hasDoubleXP = AppFig.IsFeatureEnabled("double_xp");
}

void Update() {
    if (hasDoubleXP) {
        // ...
    }
}
```

### 3. Handle Null Values

Always check for null:

```csharp
string maxLives = AppFig.GetFeatureValue("max_lives");
if (maxLives != null) {
    int lives = int.Parse(maxLives);
} else {
    int lives = 5; // Default fallback
}
```

### 4. Log Events Liberally

The more events you log, the more powerful your targeting:

```csharp
void CompleteLevel() {
    AppFig.LogEvent("level_complete", new Dictionary<string, string> {
        { "level_number", currentLevel.ToString() },
        { "time_seconds", timeElapsed.ToString() },
        { "stars", starsEarned.ToString() }
    });
}
```

### 5. Use Meaningful Event Parameters

```csharp
// ✅ Good: Descriptive parameters
AppFig.LogEvent("purchase_made", new Dictionary<string, string> {
    { "item_id", "legendary_sword" },
    { "price_usd", "9.99" },
    { "payment_method", "credit_card" }
});

// ❌ Bad: Vague parameters
AppFig.LogEvent("purchase_made", new Dictionary<string, string> {
    { "id", "123" },
    { "amt", "999" }
});
```

## Troubleshooting

### DLL not recognized by Unity

**Problem:** Unity doesn't detect the DLL or shows import errors

**Solutions:**
1. Verify `AppFigUnitySDK.dll` is in `Assets/Plugins/AppFig/` folder
2. Check the Inspector for the DLL - it should show "Plugin" as the type
3. Right-click in Project window and select "Refresh" to force Unity to re-import
4. If still not working, restart Unity Editor

### "The type or namespace name 'AppFig' could not be found"

**Problem:** Scripts can't find AppFig classes

**Solutions:**
1. Verify the DLL is properly imported (check Plugin Inspector)
2. Try reimporting: Right-click DLL → Reimport
3. Check Unity console for import errors or warnings
4. Ensure you're using Unity 2019.4 or higher (minimum supported version)

### "AppFig not initialized"

**Problem:** Calling SDK methods before `Init()` or `InitLocal()`

**Solution:**
```csharp
void Start() {
    AppFig.Init(...);  // Must be first!
    AppFig.LogEvent("app_open");  // Now OK
}
```

### Feature always returns null

**Problem:** No rules match for this feature

**Solutions:**
1. Check feature name spelling (case-sensitive)
2. Verify rules are loaded: Check console for `[AppFig] ✅ Loaded X rule(s)`
3. Log relevant events/properties before checking feature
4. Enable debug logs to see rule evaluation

### Rules not updating (Cloud Mode)

**Problem:** Changes in dashboard not reflected in app

**Solutions:**
1. Wait until auto-refresh interval passes
2. Call `AppFig.RefreshRules()` manually
3. Check console for `[AppFig] 🔄 Auto-refresh` messages
4. Verify internet connection

### Rules not loading (Local Mode)

**Problem:** `Failed to load rules from Resources/appfig_rules.json`

**Solutions:**
1. Verify file exists at `Assets/Resources/appfig_rules.json`
2. Check filename is exact (case-sensitive)
3. Validate JSON syntax
4. Make sure you're using filename without `.json` extension

## Performance Tips

### Event History Management

The SDK automatically manages event history to maintain optimal performance without manual intervention.

### Memory and CPU Usage

AppFig has minimal memory and CPU overhead, making it suitable for production Unity applications across all platforms including mobile devices. The SDK is optimized for:

- Low memory footprint
- Fast feature checks with intelligent caching
- Efficient rule evaluation

While it's safe to check features frequently, we recommend caching feature values when possible to follow best practices.

## Next Steps

- **[Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)** - All rule options
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your features
- **[Local Mode](#AppFig-docs%2FAdvanced%2Flocal-mode.md)** - Detailed local mode guide


---

<!-- SOURCE: AppFig-docs/Advanced/local-mode.md -->

# Local Mode Guide

Use AppFig without a server connection by loading rules from a JSON file.

## What is Local Mode?

Local Mode lets you use all of AppFig's powerful rule engine **without** connecting to a remote server. Perfect for:

- **Free plan** - Use feature flags at no cost
- **Offline development** - Work without internet
- **Indie developers** - Full features without subscription
- **Prototyping** - Test ideas quickly

### Event Persistence in Local Mode

**Events are persisted to disk** in local mode (same as cloud mode):

- ✅ Event history maintained across app restarts
- ✅ Rules evaluate against logged events normally
- ✅ All event-based targeting works
- ✅ Events persisted to disk automatically
- ✅ Full disk persistence (same as cloud mode)

This means event-based rules (like "show feature after 10 level completions") work the same in local mode as they do in cloud mode, with events persisting across app restarts.

## Free vs Paid Plans

| Feature | Free (Local Mode) | Paid (Cloud Mode) |
|---------|-------------------|-------------------|
| **Rule Engine** | ✅ Full power | ✅ Full power |
| **Feature flags** | ✅ Unlimited | ✅ Plan-based limits |
| **Event tracking** | ✅ All events | ✅ All events |
| **Event persistence** | ✅ Disk storage | ✅ Disk storage |
| **User/device properties** | ✅ Full support | ✅ Full support |
| **Sequences & targeting** | ✅ Everything | ✅ Everything |
| **Client-side evaluation** | ✅ Yes | ✅ Yes |
| **Rules source** | 📁 JSON file in app | ☁️ Cloud sync (CDN) |
| **Updates** | 🔨 Rebuild app | 🚀 Auto-refresh (12h default, configurable via `pollInterval`) |
| **Web dashboard** | ❌ Edit JSON | ✅ Visual rule builder |
| **Team collaboration** | ❌ | ✅ Admin/Collaborator roles |
| **Analytics** | ❌ | ✅ Usage tracking |
| **Multi-environment** | ⚠️ Manual files | ✅ Dev/Prod split |
| **Multi-tenancy** | ❌ | ✅ Unlimited tenants |
| **Draft/Publish workflow** | ❌ | ✅ |
| **Feature status toggle** | ❌ | ✅ Active/Passive |
| **CDN caching** | ❌ | ✅ Plan-based TTL |

**Bottom line:** Same evaluation engine, different workflow. Local = edit JSON files. Cloud = web dashboard with collaboration, versioning, and instant updates.

## Unity Setup

### Step 1: Create Rules File

Create a folder `Resources` in your Unity project if it doesn't exist:

```
YourProject/
  Assets/
    Resources/    ← Create this folder
      appfig_rules.json  ← Your rules file here
```

### Step 2: Add Your Rules

Copy your rules into `appfig_rules.json`:

```json
{
  "double_xp": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": [
            {
              "key": "level_complete",
              "count": {
                "operator": ">=",
                "value": 10
              }
            }
          ]
        },
        "user_properties": [],
        "user_properties_operator": "AND",
        "device": [],
        "device_operator": "AND"
      },
      "value": "on"
    }
  ],
  "max_lives": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": []
        },
        "user_properties": [],
        "user_properties_operator": "AND",
        "device": [],
        "device_operator": "AND"
      },
      "value": "5"
    }
  ]
}
```

### Step 3: Initialize SDK

```csharp
using UnityEngine;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // Initialize in local mode
        AppFig.InitLocal();

        // Use features normally
        if (AppFig.IsFeatureEnabled("double_xp")) {
            ApplyDoubleXP();
        }

        string maxLives = AppFig.GetFeatureValue("max_lives");
        Debug.Log($"Max lives: {maxLives}");
    }
}
```

### Custom Path

By default, SDK looks for `Resources/appfig_rules.json`. To use a different file:

```csharp
// Loads from Resources/my_rules.json
AppFig.InitLocal("my_rules");

// Loads from Resources/production.json
AppFig.InitLocal("production");
```

### Using AppFigHelper

1. Add `AppFigHelper` component to a GameObject
2. In Inspector:
   - **Use Local Mode**: ☑ Check this
   - **Local Rules Path**: `appfig_rules` (or your custom name)
   - **Enable Debug Logs**: ☑ Optional

3. Use anywhere:

```csharp
if (AppFigHelper.Instance.IsFeatureEnabled("double_xp")) {
    ApplyDoubleXP();
}
```

## Android Setup

### Step 1: Create Rules File

Add your rules file to your app's assets (e.g., `assets/appfig_rules.json`), using the same schema as the Unity example above.

### Step 2: Initialize SDK

```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val rulesJson = assets.open("appfig_rules.json").bufferedReader().use { it.readText() }
        AppFig.initLocal(this, rulesJson)

        // Use features normally
        if (AppFig.isFeatureEnabled("double_xp")) {
            applyDoubleXP()
        }
    }
}
```

**Note:** `rulesJson` is optional but required in practice — if you call `initLocal(context)` without it, the SDK does **not** auto-load anything from assets. It logs a warning and every feature returns `null` until you call `updateLocalRules()`.

### Updating Rules at Runtime

```kotlin
// Load new rules from disk and apply them immediately
val newRulesJson = assets.open("appfig_rules_v2.json").bufferedReader().use { it.readText() }
AppFig.updateLocalRules(newRulesJson)
```

`refreshRules()` (the cloud-mode CDN refresh) is disabled in local mode — use `updateLocalRules()` instead.

## iOS Setup

### Step 1: Create Rules File

Add your rules file to your app bundle (e.g., `appfig_rules.json`), using the same schema as the Unity example above.

### Step 2: Initialize SDK

```swift
if let path = Bundle.main.path(forResource: "appfig_rules", ofType: "json"),
   let rulesJson = try? String(contentsOfFile: path) {
    AppFig.initializeLocal(rulesJson: rulesJson)
}

// Use features normally
if AppFig.isFeatureEnabled("double_xp") {
    applyDoubleXP()
}
```

**Note:** `rulesJson` is optional but required in practice — if you call `initializeLocal()` without it, the SDK does **not** auto-load anything from the bundle. It logs a warning and every feature returns `nil`.

**Limitation:** unlike Android, the iOS SDK has no `updateLocalRules()` equivalent — there's no way to swap in new rules at runtime without re-launching the app and calling `initializeLocal()` again.

## React/Web Setup

### Step 1: Create Rules File

Add your rules file to your project (e.g., `public/appfig_rules.json`):

```json
{
  "welcome_message": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": []
        },
        "user_properties": [],
        "device": []
      },
      "value": "Welcome to our app!"
    }
  ]
}
```

### Step 2: Load Rules

```typescript
import AppFig from './AppFigReactSDK';

// Fetch your rules JSON
const response = await fetch('/appfig_rules.json');
const rules = await response.json();

// Initialize with rules override (companyId/tenantId/env/apiKey are still required)
await AppFig.init({
  companyId: 'your-company-id',
  tenantId: 'your-tenant-id',
  env: 'dev',
  apiKey: 'your-api-key',
  rulesOverride: rules
});

// Use features normally
const msg = AppFig.getConfig('welcome_message');
console.log(msg); // "Welcome to our app!"
```

**Note:** Unlike Unity/Android/iOS local mode, the React SDK doesn't have a separate offline-only local init path. `rulesOverride` still fetches rules from the network on `init()` before the override is applied — it isn't fully offline.

## Updating Rules

### During Development

Unity automatically reimports files when you save them:

1. Edit `Assets/Resources/appfig_rules.json`
2. Save the file
3. Unity reimports (watch Console)
4. Rerun your game

To test changes without restarting:

```csharp
// Reload rules from file
AppFig.RefreshRules();
```

### In Production

Rules are bundled with your app. To update:

1. Edit your rules JSON file
2. Rebuild your app
3. Distribute new build to users

**This is the main limitation of local mode** - changes require app updates.

## JSON File Structure

Your rules file must match the AppFig schema:

```json
{
  "feature_name": [
    {
      "conditions": {
        "events": { /* event conditions */ },
        "user_properties": [ /* user property conditions */ ],
        "user_properties_operator": "AND",
        "device": [ /* device conditions */ ],
        "device_operator": "AND"
      },
      "value": "your_value"
    }
  ]
}
```

[See complete schema →](#AppFig-docs%2FReference%2Frule-schema.md)

## Sample Rules

### Simple Feature Toggle

```json
{
  "new_ui": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": []
        },
        "user_properties": [],
        "device": []
      },
      "value": "on"
    }
  ]
}
```

### Event-Based Feature

```json
{
  "veteran_bonus": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": [
            {
              "key": "level_complete",
              "count": {
                "operator": ">=",
                "value": 10
              }
            }
          ]
        },
        "user_properties": [],
        "device": []
      },
      "value": "2x"
    }
  ]
}
```

### Platform-Specific Feature

```json
{
  "mobile_controls": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": []
        },
        "user_properties": [],
        "device": [
          {
            "key": "platform",
            "value": {
              "operator": "in",
              "value": ["iOS", "Android"]
            }
          }
        ]
      },
      "value": "on"
    }
  ]
}
```

## Migrating to Cloud Mode

When you're ready to upgrade:

**Unity:**

```csharp
// Before (local mode)
AppFig.InitLocal();

// After (cloud mode)
AppFig.Init("company-id", "tenant-id", "prod", "your-api-key");

// Everything else stays the same!
```

**Android:**

```kotlin
// Before (local mode)
val rulesJson = assets.open("appfig_rules.json").bufferedReader().use { it.readText() }
AppFig.initLocal(this, rulesJson)

// After (cloud mode)
AppFig.init(
    context = this,
    companyId = "company-id",
    tenantId = "tenant-id",
    env = "prod",
    apiKey = "your-api-key"
)

// Everything else stays the same!
```

**iOS:**

```swift
// Before (local mode)
if let path = Bundle.main.path(forResource: "appfig_rules", ofType: "json"),
   let rulesJson = try? String(contentsOfFile: path) {
    AppFig.initializeLocal(rulesJson: rulesJson)
}

// After (cloud mode)
AppFig.initialize(
    companyId: "company-id",
    tenantId: "tenant-id",
    env: "prod",
    apiKey: "your-api-key"
)

// Everything else stays the same!
```

**React:**

```typescript
// Before (local mode)
const rules = await fetch('/rules.json').then(r => r.json());
await AppFig.init({
  companyId: 'company-id',
  tenantId: 'tenant-id',
  env: 'dev',
  apiKey: 'your-api-key',
  rulesOverride: rules
});

// After (cloud mode)
await AppFig.init({
  companyId: 'company-id',
  tenantId: 'tenant-id',
  env: 'prod',
  apiKey: 'your-api-key'
});

// Everything else stays the same!
```

**What happens after upgrading:**
1. Rules now sync from cloud dashboard instead of local files
2. Changes take effect on the next poll (default: 12 hours, configurable via `pollInterval`) — no app rebuild needed
3. Team members can collaborate using dashboard
4. Dev/prod environments kept separate
5. Rules versioned and cached via CDN

No code changes needed beyond initialization!

## Best Practices

### 1. Version Control Your Rules

```bash
git add Assets/Resources/appfig_rules.json
git commit -m "Update feature flags"
```

Track changes to your rules over time.

### 2. Multiple Environment Files

Create separate files for different environments:

```
Assets/Resources/
  appfig_rules_dev.json
  appfig_rules_staging.json
  appfig_rules_prod.json
```

Load based on build configuration:

```csharp
#if UNITY_EDITOR
    AppFig.InitLocal("appfig_rules_dev");
#elif DEVELOPMENT_BUILD
    AppFig.InitLocal("appfig_rules_staging");
#else
    AppFig.InitLocal("appfig_rules_prod");
#endif
```

### 3. Validate Your JSON

Use a JSON validator before committing:
- [jsonlint.com](https://jsonlint.com)
- VS Code with JSON validation
- Online validators

Invalid JSON will cause the SDK to fail loading.

### 4. Document Your Features

Add comments in a separate file:

```json
// features.md
{
  "double_xp": "Enables 2x XP for veteran players (10+ levels)",
  "max_lives": "Maximum lives player can have",
  "welcome_bonus": "Bonus for new users who haven't purchased"
}
```

### 5. Use the Sample File

Copy and modify the provided sample:

```bash
cp SDKs/appfig_rules_sample.json Assets/Resources/appfig_rules.json
```

## Troubleshooting

### "Failed to load rules from Resources/appfig_rules.json" (Unity)

**Solutions:**
- Verify file exists at `Assets/Resources/appfig_rules.json`
- Check filename is exact (case-sensitive)
- Ensure you're using filename without `.json` extension in code
- Look for Unity import errors in Console

### Features return null / nil with no error (Android, iOS)

**This is expected if `rulesJson` was omitted.** Unlike Unity, `initLocal()` (Android) and `initializeLocal()` (iOS) do **not** auto-load a bundled/asset file when `rulesJson` is left out — they just log a warning and every feature returns `null`/`nil`.

**Solutions:**
- Pass `rulesJson` explicitly when calling `initLocal()` / `initializeLocal()`
- Android: call `AppFig.updateLocalRules(rulesJson)` afterward if you need to load rules later
- iOS: there's no runtime reload — re-launch and call `initializeLocal(rulesJson:)` again with the correct string

### "Failed to parse JSON"

**Solutions:**
- Validate JSON syntax with a validator
- Check for trailing commas
- Ensure proper quote characters (`"` not `"`)
- Verify structure matches schema

### "Auto-refresh disabled in LOCAL MODE"

**This is expected.** Auto-refresh only works in cloud mode. In local mode:
- Rules are loaded once at init
- Reload manually: Unity `RefreshRules()`, Android `updateLocalRules()`, iOS re-run `initializeLocal()`
- Changes require app rebuild in production

### Features not updating

**Solutions:**
- Unity: Save file and wait for reimport, then call `AppFig.RefreshRules()`
- Android: call `AppFig.updateLocalRules(newRulesJson)`
- iOS: no runtime reload API exists — re-launch the app
- Check logs for parse errors
- Verify feature names match exactly (case-sensitive)

## Next Steps

- **[Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)** - Complete schema reference
- **[Unity SDK](#AppFig-docs%2FSDK-Documentation%2Funity-integration.md)** - Unity integration guide
- **[Android SDK](#AppFig-docs%2FSDK-Documentation%2Fandroid-integration.md)** - Android integration guide
- **[iOS SDK](#AppFig-docs%2FSDK-Documentation%2Fios-integration.md)** - iOS integration guide
- **[React SDK](#AppFig-docs%2FSDK-Documentation%2Freact-web-guide.md)** - React/Web integration guide
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your rules


---

<!-- SOURCE: AppFig-docs/Advanced/testing.md -->

# Testing & Troubleshooting

Complete guide to testing your feature flags and debugging issues.

## Testing Your Rules

### 1. Enable Debug Logging

**Unity:**
```csharp
// SDK automatically logs to Console
// Look for lines starting with [AppFig]

// Check what's loaded
var debug = AppFig.GetDebugInfo();
Debug.Log($"Rules: {debug.rules.Count}");
Debug.Log($"Events: {debug.events.Count}");
```

**React:**
```typescript
await AppFig.init({
  companyId: 'your-company-id',
  tenantId: 'test-app',
  env: 'dev',
  apiKey: 'your-api-key',
  debugMode: true  // Enable verbose logging
});

// Console will show:
// [AppFig] 🔍 getConfig called for: welcome_message
// [AppFig] 🔍 Rule matches: true
// [AppFig] 🔍 Returning value: "Hello!"
```

### 2. Test Event Logging

Log events and verify they're tracked:

**Unity:**
```csharp
// Log event
AppFig.LogEvent("test_event");

// Check it was recorded
var events = AppFig.GetEventHistory();
Debug.Log($"Total events logged: {events.Count}");

// Verify specific event exists
var testEvents = events.FindAll(e => e.name == "test_event");
Debug.Log($"Found {testEvents.Count} test_event occurrences");
```

**React:**
```typescript
// Log event
AppFig.logEvent('test_event', { param: 'value' });

// Check it was recorded
const events = AppFig.getEventHistory();
console.log('Total events:', events.length);
console.log('Events:', events.map(e => e.name));
```

### 3. Test Properties

Set properties and verify they're stored:

**Unity:**
```csharp
// Set properties
AppFig.SetUserProperty("test_property", "test_value");
AppFig.SetDeviceProperty("test_device", "test_device_value");

// Check debug info
var debug = AppFig.GetDebugInfo();
Debug.Log($"User properties: {debug.userProperties.Count}");
Debug.Log($"Device properties: {debug.deviceProperties.Count}");
```

**React:**
```typescript
// Set properties
AppFig.setUserProperties({ test_property: 'test_value' });

// Check debug info
const debug = AppFig.getDebugInfo();
console.log('User properties:', debug.userProperties);
console.log('Device properties:', debug.deviceProperties);
```

### 4. Test Rule Evaluation

Create a simple test rule:

```json
{
  "test_feature": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": [
            {
              "key": "test_event",
              "count": {
                "operator": ">=",
                "value": 1
              }
            }
          ]
        },
        "user_properties": [],
        "device": []
      },
      "value": "success"
    }
  ]
}
```

Test it step-by-step:

```csharp
// Unity
// 1. Before logging event
string value1 = AppFig.GetFeatureValue("test_feature");
Debug.Log($"Before event: {value1}"); // null

// 2. Log the event
AppFig.LogEvent("test_event");

// 3. After logging event
string value2 = AppFig.GetFeatureValue("test_feature");
Debug.Log($"After event: {value2}"); // "success"
```

### 5. Test Time Windows

```json
{
  "recent_activity": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": [
            {
              "key": "app_open",
              "count": { "operator": ">=", "value": 1 },
              "within_last_days": 7
            }
          ]
        },
        "user_properties": [],
        "device": []
      },
      "value": "active"
    }
  ]
}
```

Test:
```csharp
AppFig.LogEvent("app_open");
string status = AppFig.GetFeatureValue("recent_activity");
Debug.Log($"Status: {status}"); // "active"

// To test old events, you can't easily fake timestamps
// Best approach: Use local mode and modify the SDK for testing
```

## Common Issues

### Issue: Features Always Return Null

**Symptoms:**
- `GetFeatureValue()` always returns `null`
- `IsFeatureEnabled()` always returns `false`

**Diagnosis:**
```csharp
// Check if rules are loaded
var debug = AppFig.GetDebugInfo();
Debug.Log($"Rules loaded: {debug.rules.Count}");

if (debug.rules.Count == 0) {
    Debug.LogError("No rules loaded!");
}
```

**Common causes:**

1. **Rules not loaded (Cloud Mode)**
   - Check console for `[AppFig] ✅ Loaded X rule(s)` message
   - Verify internet connection
   - Check API key is correct
   - Verify company/tenant IDs

2. **Rules not loaded (Local Mode)**
   - Verify file exists at `Assets/Resources/appfig_rules.json`
   - Check for JSON parse errors in Console
   - Validate JSON syntax

3. **Feature name mismatch**
   ```csharp
   // Feature in JSON: "doubleXP"
   // Your code: "double_xp"  ← Case mismatch!

   // Solution: Match exactly
   AppFig.GetFeatureValue("doubleXP");
   ```

4. **Conditions don't match**
   ```csharp
   // Rule requires 10 level_complete events
   // But you only logged 5

   // Check event count
   var events = AppFig.GetEventHistory();
   var levelEvents = events.FindAll(e => e.name == "level_complete");
   Debug.Log($"Level events: {levelEvents.Count}");
   ```

### Issue: Rules Not Updating (Cloud Mode)

**Symptoms:**
- Changed rules in dashboard
- App still uses old rules

**Solutions:**

1. **Wait for auto-refresh**
   - Default: 12 hours (43200000ms), configurable via the `pollInterval` init parameter
   - Check console for `[AppFig] 🔄 Auto-refresh` message

2. **Manual refresh**
   ```csharp
   AppFig.RefreshRules();
   ```

3. **Check auto-refresh is enabled**
   - Pass `autoRefresh: true` to `Init()` (default is already `true`)
   - `SetAutoRefresh()` still exists but is deprecated — use the `autoRefresh` parameter in `Init()` instead

4. **Clear cache**
   ```csharp
   // Restart app to clear cached rules
   // Or call RefreshRules()
   ```

### Issue: Initialization Failed

**Symptoms:**
- `[AppFig] Failed to load rules` in console
- SDK methods throw errors

**Cloud Mode Solutions:**

1. **Check credentials**
   ```csharp
   // Verify all parameters are correct - use Firestore document IDs
   AppFig.Init(
       "acmegames",      // Company ID from Firestore (not display name)
       "spaceshooter",   // Tenant ID from Firestore (not display name)
       "prod",           // or "dev"
       "your-api-key"    // API key from dashboard
   );
   ```

2. **Check network**
   - Verify internet connection
   - Check firewall isn't blocking requests
   - Try from different network

3. **Check endpoint**
   - Console will show URL being fetched
   - Verify URL is accessible in browser

**Local Mode Solutions:**

1. **Check file path**
   ```csharp
   // File must be at: Assets/Resources/appfig_rules.json
   // Code should use: AppFig.InitLocal("appfig_rules")
   // NOT: AppFig.InitLocal("appfig_rules.json")
   ```

2. **Validate JSON**
   - Copy contents to [jsonlint.com](https://jsonlint.com)
   - Fix any syntax errors

3. **Check Resources folder**
   - Folder must be named exactly "Resources"
   - Must be under Assets folder
   - Unity should recognize it (shows special folder icon)

### Issue: Events Not Counted

**Symptoms:**
- Logged 10 events
- Rule says count >= 10
- Feature still returns null

**Diagnosis:**
```csharp
// Check event history
var events = AppFig.GetEventHistory();
Debug.Log($"Total events: {events.Count}");

// Check specific event
var targetEvents = events.FindAll(e => e.name == "level_complete");
Debug.Log($"level_complete events: {targetEvents.Count}");

// Check if time window is restricting
// Rules with within_last_days will filter out old events
```

**Solutions:**

1. **Event name mismatch**
   ```csharp
   // Rule: "level_complete"
   // Code: "levelComplete"  ← Mismatch!

   // Fix: Match exactly
   AppFig.LogEvent("level_complete");
   ```

2. **Time window restriction**
   ```json
   {
     "key": "level_complete",
     "count": { "operator": ">=", "value": 10 },
     "within_last_days": 7  // ← Only counts last 7 days!
   }
   ```

3. **Parameter mismatch**
   ```json
   // Rule requires:
   {
     "key": "purchase",
     "param": {
       "amount": {
         "operator": ">",
         "value": 10
       }
     }
   }
   ```

   ```csharp
   // Your code doesn't include param
   AppFig.LogEvent("purchase");  // Won't match!

   // Fix: Include required parameter
   AppFig.LogEvent("purchase", new Dictionary<string, string> {
       { "amount", "15" }
   });
   ```

### Issue: Properties Not Working

**Symptoms:**
- Set user/device property
- Rule checks property
- Feature still returns null

**Diagnosis:**
```csharp
// Check properties are set
var debug = AppFig.GetDebugInfo();
Debug.Log($"User props: {string.Join(", ", debug.userProperties.Keys)}");
Debug.Log($"Device props: {string.Join(", ", debug.deviceProperties.Keys)}");

// Check specific property value
if (debug.userProperties.TryGetValue("subscription_tier", out string value)) {
    Debug.Log($"subscription_tier = {value}");
} else {
    Debug.Log("subscription_tier not set!");
}
```

**Solutions:**

1. **Property not set**
   ```csharp
   // Make sure you set it before checking feature
   AppFig.SetUserProperty("subscription_tier", "premium");

   // Then check feature
   if (AppFig.IsFeatureEnabled("premium_features")) {
       // ...
   }
   ```

2. **Property name/value mismatch**
   ```json
   // Rule checks:
   {
     "key": "subscription_tier",
     "value": { "operator": "==", "value": "premium" }
   }
   ```

   ```csharp
   // Your code:
   AppFig.SetUserProperty("subscriptionTier", "Premium");
   // Wrong! Case matters for both key and value

   // Fix:
   AppFig.SetUserProperty("subscription_tier", "premium");
   ```

3. **Type mismatch**
   ```json
   // Rule expects string "true"
   {
     "key": "is_verified",
     "value": { "operator": "==", "value": "true" }
   }
   ```

   ```csharp
   // Your code:
   AppFig.SetUserProperty("is_verified", "True");  // Capital T!

   // Fix: Match exactly
   AppFig.SetUserProperty("is_verified", "true");
   ```

## Testing Workflows

### Test Sequence: New User Flow

```csharp
// 1. Reset session (simulate new user)
AppFig.ResetSession();

// 2. Initialize
AppFig.Init(...);

// 3. Check welcome bonus (should be available)
string bonus1 = AppFig.GetFeatureValue("welcome_bonus");
Debug.Log($"New user bonus: {bonus1}"); // "on"

// 4. Make a purchase
AppFig.LogEvent("purchase_made");

// 5. Check bonus again (should be gone if rule excludes purchasers)
string bonus2 = AppFig.GetFeatureValue("welcome_bonus");
Debug.Log($"After purchase bonus: {bonus2}"); // null
```

### Test Sequence: Engagement Flow

```csharp
// 1. User opens app
AppFig.LogEvent("app_open");

// 2. Completes levels
for (int i = 0; i < 10; i++) {
    AppFig.LogEvent("level_complete");
}

// 3. Check veteran bonus (should be available at 10 completions)
string bonus = AppFig.GetFeatureValue("veteran_bonus");
Debug.Log($"Veteran bonus: {bonus}"); // "2x"
```

### Test Sequence: Platform Check

```csharp
// 1. Check auto-detected platform
var debug = AppFig.GetDebugInfo();
string platform = debug.deviceProperties["platform"];
Debug.Log($"Platform: {platform}");

// 2. Check platform-specific feature
string controls = AppFig.GetFeatureValue("mobile_controls");
Debug.Log($"Mobile controls: {controls}");

// Should be "on" for iOS/Android, null for desktop
```

## Performance Testing

### Measure Feature Check Speed

```csharp
var stopwatch = System.Diagnostics.Stopwatch.StartNew();

// First call (evaluates rules)
string value1 = AppFig.GetFeatureValue("test_feature");

stopwatch.Stop();
Debug.Log($"First call: {stopwatch.ElapsedTicks} ticks");

stopwatch.Restart();

// Second call (cached)
string value2 = AppFig.GetFeatureValue("test_feature");

stopwatch.Stop();
Debug.Log($"Second call: {stopwatch.ElapsedTicks} ticks");

// Expected results:
// First call: ~1000-10000 ticks (~100 microseconds)
// Second call: ~10-100 ticks (~0.5 microseconds)
```

### Memory Usage

```csharp
// Get memory before
long memBefore = System.GC.GetTotalMemory(false);

// Initialize SDK
AppFig.Init(...);

// Get memory after
long memAfter = System.GC.GetTotalMemory(false);
long memUsed = memAfter - memBefore;

Debug.Log($"SDK memory usage: {memUsed / 1024} KB");

// Typical: 100-500 KB depending on rule complexity
```

## Troubleshooting Checklist

When features don't work as expected:

- [ ] Rules loaded? Check console for load confirmation
- [ ] Feature name matches exactly? (case-sensitive)
- [ ] Events logged? Check event history
- [ ] Properties set? Check debug info
- [ ] Time windows correct? Recent enough events?
- [ ] Parameters match? Check event params
- [ ] Operators correct? `>=` not `>`, `==` not `=`
- [ ] AND/OR logic correct? Check operators
- [ ] NOT logic intended? Check `not` field
- [ ] Multiple rules? First match wins
- [ ] Cache cleared? Call RefreshRules() or restart

## Next Steps

- **[Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)** - Complete rule reference
- **[Unity SDK](#AppFig-docs%2FSDK-Documentation%2Funity-integration.md)** - Unity integration
- **[React SDK](#AppFig-docs%2FSDK-Documentation%2Freact-web-guide.md)** - React integration
- **[Local Mode](#AppFig-docs%2FAdvanced%2Flocal-mode.md)** - Local mode setup


---

<!-- SOURCE: AppFig-docs/Reference/ab-testing.md -->

# A/B Testing Reference

AppFig extends its rule engine with a lightweight, device-side multivariate testing (A/B testing) mode. It reuses the existing targeting system (events, sequences, user properties, device conditions) — there is no separate "experiment" concept to learn.

## Overview

A/B testing in AppFig:

- Assigns users to experiment variants **deterministically**, with zero network calls at evaluation time
- **Persists the assignment** so a user doesn't flip to a different variant mid-session
- Pushes the active variant assignments to your analytics provider via a single unified user property

This is not a separate rule type. It's a toggle on the **Return Value** section of an existing rule — all targeting logic (who the rule applies to) stays exactly the same whether the rule returns a static value or an A/B test.

## Data Model

Each rule already has a `conditions` block (see [Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)) that decides *who* the rule applies to. What changes is the return side — a rule returns **either**:

```json
{ "value": any }
```

**or**

```json
{
  "ab_test": {
    "experiment_key": "string",
    "variants": [
      { "name": "string", "weight": number, "value": any }
    ]
  }
}
```

A rule never has both `value` and `ab_test` — the dashboard toggle decides which one a rule stores.

**Fields:**
- `experiment_key` (required) - Unique identifier for the experiment. Used in analytics and for deterministic assignment.
- `variants` (required) - Array of possible outcomes for users who match this rule's conditions.
  - `name` (required) - Variant identifier, e.g. `"control"`, `"treatment"`
  - `weight` (required) - Relative weight for probabilistic assignment. Weights don't need to sum to 100 — they're normalized (e.g. `1`/`1` and `50`/`50` produce the same 50/50 split).
  - `value` (required) - What `getFeatureValue()` / `getConfig()` returns for a user assigned to this variant. Can be any JSON-serializable type, same as a normal rule's `value`.

**Example: 50/50 button color test**

```json
{
  "checkout_button_color": [
    {
      "conditions": {
        "events": { "mode": "simple", "operator": "AND", "items": [] },
        "user_properties": [],
        "user_properties_operator": "AND",
        "device": [],
        "device_operator": "AND"
      },
      "ab_test": {
        "experiment_key": "checkout_button_color_test",
        "variants": [
          { "name": "control", "weight": 1, "value": "blue" },
          { "name": "treatment", "weight": 1, "value": "green" }
        ]
      }
    }
  ]
}
```

**Example: Weighted rollout (10% treatment)**

```json
{
  "new_onboarding_flow": [
    {
      "conditions": {
        "events": { "mode": "simple", "operator": "AND", "items": [] },
        "user_properties": [],
        "user_properties_operator": "AND",
        "device": [],
        "device_operator": "AND"
      },
      "ab_test": {
        "experiment_key": "onboarding_v2_rollout",
        "variants": [
          { "name": "control", "weight": 90, "value": "legacy" },
          { "name": "treatment", "weight": 10, "value": "v2" }
        ]
      }
    }
  ]
}
```

## Dashboard: Toggle-Based A/B Test Mode

In the dashboard rule builder, the condition builder (events, user properties, device) is untouched — A/B testing only changes the **Return Value** section:

- **Toggle OFF** (default) - Single value input, same as any normal rule
- **Toggle ON** (`Run as A/B Test`) - Replaces the value input with:
  - An experiment key field
  - A variants table (name, weight, value per row)

This keeps targeting logic unified: you build your conditions once, and can flip a rule between "static value" and "A/B test" without redoing any targeting.

## Assignment Behavior

- **Deterministic:** a given user + `experiment_key` always resolves to the same variant as long as the assignment is valid, with no network round-trip needed.
- **Sticky per session:** once a user is assigned a variant, they keep that variant for the rest of the session — they won't flip between variants mid-session even if you republish rules.
- **Re-evaluated on next app open:** a fresh assignment is computed on the next app launch. This means changing variant weights, or changing the targeting conditions on the rule, can move a user into a different variant (or out of the experiment) starting from their next session — but never mid-session.
- **No experiment lifecycle on AppFig:** AppFig does not track experiment start/end dates, statistical significance, or winning variants. It only assigns and persists variants — you own experiment analysis in your analytics tool.

**Typical workflow:** toggle a rule's Return Value to A/B Test → users start getting variants → you analyze results in your analytics provider → once you have a winner, toggle back to a static value and write the winning value directly.

## Analytics Integration

AppFig does not send experiment data to your analytics provider on your behalf — it exposes the current assignments as a **user property** that you attach to your own analytics SDK calls.

- **Property name:** `appfig_experiments`
- **Format:** pipe-separated `experiment_key:variant_name` pairs

```
"exp_key1:variant1|exp_key2:variant2"
```

If a user is in two active experiments (`checkout_button_color_test` → `treatment`, `onboarding_v2_rollout` → `control`), `appfig_experiments` would be:

```
"checkout_button_color_test:treatment|onboarding_v2_rollout:control"
```

**You are responsible for:**
- Reading `appfig_experiments` from the SDK
- Setting it as a user property on your analytics provider (e.g. Amplitude, Mixpanel) before or alongside your own analytics events
- Parsing the pipe-separated format if your analytics tool needs individual experiment/variant fields rather than one combined string

AppFig does not integrate directly with any specific analytics vendor — this property is the sole hand-off point.

## Next Steps

- **[Rule Schema Reference](#AppFig-docs%2FReference%2Frule-schema.md)** - Full targeting condition reference (applies identically to A/B test rules)
- **[Rule Prioritization](#AppFig-docs%2FReference%2Frule-prioritization.md)** - Control which RuleSet (static or A/B test) wins when several could match
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your rules


---

<!-- SOURCE: AppFig-docs/Reference/rule-prioritization.md -->

# Rule Prioritization (RuleSet Ordering)

Lets you control which rule "wins" when multiple rules could apply to the same user, by setting an explicit evaluation order for the RuleSets inside a feature.

## How Evaluation Works

- Each feature can have multiple **RuleSets** — rule blocks, each with their own conditions and return value (a static `value` or an [A/B test](#AppFig-docs%2FReference%2Fab-testing.md))
- RuleSets are evaluated **top to bottom**, in the order shown in the dashboard UI
- The **first RuleSet whose conditions match** the user wins — its value (or A/B test assignment) is returned and evaluation stops
- If no RuleSet matches, no value is returned for that feature (`getFeatureValue()` / `getConfig()` returns `null`/`nil`)

This is the same "first match wins" behavior already described in [Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md) and [Core Concepts](#AppFig-docs%2FGetting%20Started%2Fconcepts.md) — this page documents how you control the order.

**Example:**

```json
Feature: "veteran_bonus"
  RuleSet 1 (Priority #1): IF user completed 50+ levels → value = "3x"
  RuleSet 2 (Priority #2): IF user completed 10+ levels → value = "2x"
  (No match) → feature returns null
```

A user with 60 completed levels matches both RuleSets, but RuleSet 1 is evaluated first and wins — they get `"3x"`, not `"2x"`.

## Dashboard Controls

- **Priority badge** - each RuleSet displays a numbered badge (`Priority #1`, `Priority #2`, ...) showing its position in the evaluation order
- **Up / Down buttons** - reorder a RuleSet by moving it up or down relative to its siblings
  - The "up" button is disabled on the first RuleSet
  - The "down" button is disabled on the last RuleSet
- **Info banner** - a short reminder above the RuleSet list: "RuleSets are evaluated top to bottom. The first matching rule's value is applied."

Reordering takes effect the next time rules are published — no separate "activate ordering" step.

## Under the Hood

- **No new database fields.** Priority is simply the array position of the RuleSet as stored — there's no explicit `priority` integer field to keep in sync.
- **No backend/serializer changes.** The existing pipeline already preserves array order end-to-end (Firestore → JSON → CDN).
- **No SDK changes.** SDKs already iterate RuleSets sequentially and return on the first match (see [Rule Schema](#AppFig-docs%2FReference%2Frule-schema.md)), so reordering in the dashboard is immediately effective once published — no SDK update is required on any platform.

## Scope

This ordering applies **within a feature** — it controls which of that feature's own RuleSets wins. It does **not** control the order in which different features are evaluated relative to each other; that's left entirely to your app's own logic (e.g. which `getFeatureValue()` calls you make and in what order).

## Next Steps

- **[Rule Schema Reference](#AppFig-docs%2FReference%2Frule-schema.md)** - Full condition and return-value reference
- **[A/B Testing](#AppFig-docs%2FReference%2Fab-testing.md)** - Use A/B test variants as a RuleSet's return value
- **[Core Concepts](#AppFig-docs%2FGetting%20Started%2Fconcepts.md)** - "First Match Wins" principle


---

<!-- SOURCE: AppFig-docs/Reference/rule-schema.md -->

# Rule Schema Reference

Complete reference for the AppFig rule schema (v2.0.0).

## Rule Structure

A feature can have one or more rules. Each rule has:

```json
{
  "conditions": {
    "events": { /* event conditions */ },
    "user_properties": [ /* user property conditions */ ],
    "user_properties_operator": "AND" | "OR",
    "device": [ /* device conditions */ ],
    "device_operator": "AND" | "OR"
  },
  "value": any  // What to return if conditions match
}
```

**All three condition types must pass for a rule to match** (AND logic between types).

## Event Conditions

Events can be checked in two modes: **Simple** and **Sequence**.

### Simple Mode

Check if events exist with certain criteria.

```json
{
  "mode": "simple",
  "operator": "AND" | "OR",
  "items": [
    {
      "key": "event_name",
      "count": {
        "operator": "==" | "!=" | ">" | "<" | ">=" | "<=",
        "value": number
      },
      "within_last_days": number,
      "param": {
        "param_name": {
          "operator": "==" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not_in" | "contains",
          "value": any
        }
      },
      "not": boolean
    }
  ]
}
```

**Fields:**
- `mode` - Must be `"simple"`
- `operator` - How to combine items: `"AND"` (all must match) or `"OR"` (any can match)
- `items` - Array of event conditions

**Item fields:**
- `key` (required) - Event name to match
- `count` (optional) - How many times event must occur
- `within_last_days` (optional) - Only count events in last N days
- `param` (optional) - Match event parameters
- `not` (optional) - Negate the condition (default: false)

**Example: User completed 10+ levels AND made a purchase**

```json
{
  "mode": "simple",
  "operator": "AND",
  "items": [
    {
      "key": "level_complete",
      "count": {
        "operator": ">=",
        "value": 10
      }
    },
    {
      "key": "purchase_made",
      "count": {
        "operator": ">=",
        "value": 1
      }
    }
  ]
}
```

**Example: User opened app recently OR completed tutorial**

```json
{
  "mode": "simple",
  "operator": "OR",
  "items": [
    {
      "key": "app_open",
      "count": {
        "operator": ">=",
        "value": 1
      },
      "within_last_days": 7
    },
    {
      "key": "tutorial_complete",
      "count": {
        "operator": ">=",
        "value": 1
      }
    }
  ]
}
```

**Example: User has NOT made a purchase**

```json
{
  "mode": "simple",
  "operator": "AND",
  "items": [
    {
      "key": "purchase_made",
      "count": {
        "operator": ">=",
        "value": 1
      },
      "not": true
    }
  ]
}
```

### Sequence Mode

Check if events occurred in a specific order.

```json
{
  "mode": "sequence",
  "ordering": "direct" | "indirect",
  "steps": [
    {
      "key": "event_name",
      "count": {
        "operator": "==" | "!=" | ">" | "<" | ">=" | "<=",
        "value": number
      },
      "within_last_days": number,
      "param": {
        "param_name": {
          "operator": "==" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not_in" | "contains",
          "value": any
        }
      }
    }
  ]
}
```

**Fields:**
- `mode` - Must be `"sequence"`
- `ordering` - Sequence type:
  - `"direct"` - Events must be consecutive (no other events between them)
  - `"indirect"` - Events must be in order but can have other events between
- `steps` - Array of events in required order

**Example: Onboarding flow (direct)**

```json
{
  "mode": "sequence",
  "ordering": "direct",
  "steps": [
    { "key": "tutorial_start" },
    { "key": "tutorial_step_1" },
    { "key": "tutorial_step_2" },
    { "key": "tutorial_complete" }
  ]
}
```

**Example: Engagement pattern (indirect)**

```json
{
  "mode": "sequence",
  "ordering": "indirect",
  "steps": [
    { "key": "app_open" },
    { "key": "level_complete" },
    { "key": "purchase_made" }
  ]
}
```

## User Property Conditions

Check attributes of the user.

```json
{
  "user_properties": [
    {
      "key": "property_name",
      "value": {
        "operator": "==" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not_in" | "contains",
        "value": any
      },
      "not": boolean
    }
  ],
  "user_properties_operator": "AND" | "OR"
}
```

**Fields:**
- `key` (required) - Property name
- `value.operator` (required) - Comparison operator
- `value.value` (required) - Value to compare against
- `not` (optional) - Negate the condition (default: false)
- `user_properties_operator` - How to combine multiple properties (default: `"AND"`)

**Example: Premium users**

```json
{
  "user_properties": [
    {
      "key": "subscription_tier",
      "value": {
        "operator": "==",
        "value": "premium"
      }
    }
  ],
  "user_properties_operator": "AND"
}
```

**Example: Premium OR trial users**

```json
{
  "user_properties": [
    {
      "key": "subscription_tier",
      "value": {
        "operator": "==",
        "value": "premium"
      }
    },
    {
      "key": "trial_active",
      "value": {
        "operator": "==",
        "value": "true"
      }
    }
  ],
  "user_properties_operator": "OR"
}
```

**Example: Account older than 30 days**

```json
{
  "user_properties": [
    {
      "key": "account_age_days",
      "value": {
        "operator": ">=",
        "value": 30
      }
    }
  ]
}
```

## Device Conditions

Check device/platform attributes (auto-detected by SDK).

```json
{
  "device": [
    {
      "key": "property_name",
      "value": {
        "operator": "==" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not_in" | "contains",
        "value": any
      },
      "not": boolean
    }
  ],
  "device_operator": "AND" | "OR"
}
```

**Auto-detected device properties:**

| Property | Example Values | Description |
|----------|---------------|-------------|
| `platform` | `"iOS"`, `"Android"`, `"Windows"`, `"macOS"` | Operating system |
| `os_version` | `"17.0"`, `"13"`, `"11.0"` | OS version string |
| `language` | `"en-US"`, `"ja"`, `"es-ES"` | System language |
| `country` | `"US"`, `"JP"`, `"GB"` | Country code (auto- detected from CDN) |
| `timezone` | `"America/Los_Angeles"`, `"Asia/Tokyo"` | Timezone identifier |
| `device_brand` | `"Apple"`, `"Samsung"`, `"Google"` | Device manufacturer |
| `device_model` | `"iPhone 15"`, `"Galaxy S23"` | Device model |
| `app_version` | `"1.2.3"` | App version (if set manually) |

**Example: Mobile platforms only**

```json
{
  "device": [
    {
      "key": "platform",
      "value": {
        "operator": "in",
        "value": ["iOS", "Android"]
      }
    }
  ]
}
```

**Example: US users only**

```json
{
  "device": [
    {
      "key": "country",
      "value": {
        "operator": "==",
        "value": "US"
      }
    }
  ]
}
```

**Example: Exclude old OS versions**

```json
{
  "device": [
    {
      "key": "os_version",
      "value": {
        "operator": ">=",
        "value": "15.0"
      }
    }
  ]
}
```

## Operators

### Comparison Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `==` | Equals | `"premium" == "premium"` → true |
| `!=` | Not equals | `"free" != "premium"` → true |
| `>` | Greater than | `10 > 5` → true |
| `<` | Less than | `5 < 10` → true |
| `>=` | Greater or equal | `10 >= 10` → true |
| `<=` | Less or equal | `5 <= 10` → true |

**Type handling:**
- Strings compared lexicographically
- Numbers parsed and compared numerically
- Booleans compared as strings

### Array Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `in` | Value in array | `"iOS" in ["iOS", "Android"]` → true |
| `not_in` | Value not in array | `"Windows" not_in ["iOS", "Android"]` → true |

### String Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `contains` | String contains substring | `"hello world" contains "world"` → true |

## Value Types

The `value` field can be any JSON-serializable type:

**Boolean:**
```json
{ "value": "on" }  // Checked with IsFeatureEnabled()
{ "value": "off" }
```

**String:**
```json
{ "value": "Welcome back!" }
```

**Number:**
```json
{ "value": "5" }    // Stored as string, parse in your app
{ "value": "99.99" }
```

**Object:**
```json
{
  "value": {
    "max_retries": 3,
    "timeout_ms": 5000,
    "enabled": true
  }
}
```

**Array:**
```json
{
  "value": ["option1", "option2", "option3"]
}
```

## Complete Example

**Feature:** "premium_features"

**Rules:**
1. Show "full" premium to paying users
2. Show "trial" premium to trial users who completed onboarding
3. Show nothing to everyone else (no matching rule)

```json
{
  "premium_features": [
    {
      "conditions": {
        "events": {
          "mode": "simple",
          "operator": "AND",
          "items": []
        },
        "user_properties": [
          {
            "key": "subscription_tier",
            "value": {
              "operator": "==",
              "value": "premium"
            }
          }
        ],
        "user_properties_operator": "AND",
        "device": [],
        "device_operator": "AND"
      },
      "value": "full"
    },
    {
      "conditions": {
        "events": {
          "mode": "sequence",
          "ordering": "indirect",
          "steps": [
            { "key": "onboarding_start" },
            { "key": "onboarding_complete" }
          ]
        },
        "user_properties": [
          {
            "key": "trial_active",
            "value": {
              "operator": "==",
              "value": "true"
            }
          }
        ],
        "user_properties_operator": "AND",
        "device": [],
        "device_operator": "AND"
      },
      "value": "trial"
    }
  ]
}
```

## Empty Conditions

To match all users (always-on feature), use empty condition arrays:

```json
{
  "conditions": {
    "events": {
      "mode": "simple",
      "operator": "AND",
      "items": []
    },
    "user_properties": [],
    "user_properties_operator": "AND",
    "device": [],
    "device_operator": "AND"
  },
  "value": "on"
}
```

This rule always matches and returns `"on"`.

## Next Steps

- **[A/B Testing](#AppFig-docs%2FReference%2Fab-testing.md)** - Use a rule's return value to run an experiment instead of a static value
- **[Rule Prioritization](#AppFig-docs%2FReference%2Frule-prioritization.md)** - Control which RuleSet wins when several could match
- **[Testing Guide](#AppFig-docs%2FAdvanced%2Ftesting.md)** - Test your rules
- **[Unity SDK](#AppFig-docs%2FSDK-Documentation%2Funity-integration.md)** - Use rules in Unity
- **[React SDK](#AppFig-docs%2FSDK-Documentation%2Freact-web-guide.md)** - Use rules in React
