From 53e7d18ee6312393bb4564e2bdbc95b2cefb1d53 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 19 Jan 2026 17:47:27 +0000 Subject: [PATCH] research docs & vibed UI --- ENTERPRISE_FEATURE_ANALYSIS.md | 417 +++++++++++++ ENTERPRISE_PRD.md | 428 ++++++++++++++ .../components/select-plan-onboarding.tsx | 20 + .../workspaces/new/plan/page.tsx | 29 + apps/web/app/page.tsx | 5 + .../images/customer-logos/cal-logo-light.svg | 9 + .../images/customer-logos/ethereum-logo.png | Bin 0 -> 7240 bytes .../images/customer-logos/flixbus-white.svg | 38 ++ .../web/images/customer-logos/github-logo.png | Bin 0 -> 27651 bytes .../images/customer-logos/pigment-logo.webp | Bin 0 -> 12154 bytes apps/web/images/customer-logos/siemens.png | Bin 0 -> 7797 bytes .../university-of-copenhegen.png | Bin 0 -> 54140 bytes apps/web/modules/auth/signup/page.tsx | 2 +- .../modules/ee/billing/api/lib/constants.ts | 124 ++-- .../ee/billing/components/overage-card.tsx | 292 +++++++++ .../ee/billing/components/pricing-card.tsx | 345 ++++++----- .../ee/billing/components/pricing-table.tsx | 555 ++++++++++++------ .../billing/components/select-plan-card.tsx | 116 ++++ .../ee/billing/components/settings-id.tsx | 36 ++ apps/web/modules/ee/billing/page.tsx | 4 +- apps/web/tailwind.config.js | 5 + 21 files changed, 2056 insertions(+), 369 deletions(-) create mode 100644 ENTERPRISE_FEATURE_ANALYSIS.md create mode 100644 ENTERPRISE_PRD.md create mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/components/select-plan-onboarding.tsx create mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/page.tsx create mode 100644 apps/web/images/customer-logos/cal-logo-light.svg create mode 100644 apps/web/images/customer-logos/ethereum-logo.png create mode 100644 apps/web/images/customer-logos/flixbus-white.svg create mode 100644 apps/web/images/customer-logos/github-logo.png create mode 100644 apps/web/images/customer-logos/pigment-logo.webp create mode 100644 apps/web/images/customer-logos/siemens.png create mode 100644 apps/web/images/customer-logos/university-of-copenhegen.png create mode 100644 apps/web/modules/ee/billing/components/overage-card.tsx create mode 100644 apps/web/modules/ee/billing/components/select-plan-card.tsx create mode 100644 apps/web/modules/ee/billing/components/settings-id.tsx diff --git a/ENTERPRISE_FEATURE_ANALYSIS.md b/ENTERPRISE_FEATURE_ANALYSIS.md new file mode 100644 index 0000000000..e356712bd9 --- /dev/null +++ b/ENTERPRISE_FEATURE_ANALYSIS.md @@ -0,0 +1,417 @@ +# Enterprise Feature Access: Status Quo Analysis + +## Executive Summary + +Formbricks currently uses **two completely different mechanisms** to gate enterprise features depending on deployment type: + +| Deployment | Gating Mechanism | Activation | Feature Control | +|------------|------------------|------------|-----------------| +| **Cloud** (`IS_FORMBRICKS_CLOUD=1`) | Billing Plan (`organization.billing.plan`) | Stripe subscription | Plan-based (FREE/STARTUP/CUSTOM) | +| **On-Premise** | License Key (`ENTERPRISE_LICENSE_KEY`) | License API validation | License feature flags | + +This dual approach creates **significant complexity**, **code duplication**, and **inconsistent behavior** across the codebase. + +--- + +## 1. Core Architecture + +### 1.1 Cloud (Formbricks Cloud) + +**Source of Truth:** `organization.billing.plan` + +```typescript +// packages/database/zod/organizations.ts +plan: z.enum(["free", "startup", "scale", "enterprise"]).default("free") +``` + +**Plans and Limits:** +- `FREE`: 3 projects, 1,500 responses/month, 2,000 MIU +- `STARTUP`: 3 projects, 5,000 responses/month, 7,500 MIU +- `CUSTOM`: Unlimited (negotiated limits) + +**Activation:** Stripe webhook updates `organization.billing` on checkout/subscription events. + +### 1.2 On-Premise (Self-Hosted) + +**Source of Truth:** `ENTERPRISE_LICENSE_KEY` environment variable + +**License Features Schema:** +```typescript +// apps/web/modules/ee/license-check/types/enterprise-license.ts +{ + isMultiOrgEnabled: boolean, + contacts: boolean, + projects: number | null, + whitelabel: boolean, + removeBranding: boolean, + twoFactorAuth: boolean, + sso: boolean, + saml: boolean, + spamProtection: boolean, + ai: boolean, + auditLogs: boolean, + multiLanguageSurveys: boolean, + accessControl: boolean, + quotas: boolean, +} +``` + +**Activation:** License key validated against `https://ee.formbricks.com/api/licenses/check` (cached for 24h, grace period of 3 days). + +--- + +## 2. Feature Gating Patterns + +### 2.1 Pattern A: Dual-Path Check (Most Common) + +Features that need **both** Cloud billing **and** on-premise license checks: + +```typescript +// apps/web/modules/ee/license-check/lib/utils.ts +const getFeaturePermission = async (billingPlan, featureKey) => { + const license = await getEnterpriseLicense(); + + if (IS_FORMBRICKS_CLOUD) { + return license.active && billingPlan !== PROJECT_FEATURE_KEYS.FREE; + } else { + return license.active && !!license.features?.[featureKey]; + } +}; +``` + +**Used by:** +- `getRemoveBrandingPermission()` - Remove branding +- `getWhiteLabelPermission()` - Whitelabel features +- `getBiggerUploadFileSizePermission()` - Large file uploads +- `getIsSpamProtectionEnabled()` - reCAPTCHA spam protection +- `getMultiLanguagePermission()` - Multi-language surveys +- `getAccessControlPermission()` - Teams & roles +- `getIsQuotasEnabled()` - Quota management +- `getOrganizationProjectsLimit()` - Project limits + +### 2.2 Pattern B: License-Only Check + +Features checked **only** against license (works same for cloud and on-premise): + +```typescript +// apps/web/modules/ee/license-check/lib/utils.ts +const getSpecificFeatureFlag = async (featureKey) => { + const licenseFeatures = await getLicenseFeatures(); + if (!licenseFeatures) return false; + return licenseFeatures[featureKey] ?? false; +}; +``` + +**Used by:** +- `getIsMultiOrgEnabled()` - Multiple organizations +- `getIsContactsEnabled()` - Contacts & segments +- `getIsTwoFactorAuthEnabled()` - 2FA +- `getIsSsoEnabled()` - SSO +- `getIsAuditLogsEnabled()` - Audit logs + +### 2.3 Pattern C: Cloud-Only (No License Check) + +Features available only on Cloud, gated purely by billing plan: + +```typescript +// apps/web/modules/survey/lib/permission.ts +export const getExternalUrlsPermission = async (billingPlan) => { + if (IS_FORMBRICKS_CLOUD) return billingPlan !== PROJECT_FEATURE_KEYS.FREE; + return true; // Always allowed on self-hosted +}; +``` + +**Used by:** +- External URLs permission +- Survey follow-ups (Custom plan only) + +### 2.4 Pattern D: On-Premise Only (Disabled on Cloud) + +Features explicitly disabled on Cloud: + +```typescript +// apps/web/modules/ee/license-check/lib/utils.ts +export const getIsSamlSsoEnabled = async () => { + if (IS_FORMBRICKS_CLOUD) return false; // Never on Cloud + const licenseFeatures = await getLicenseFeatures(); + return licenseFeatures.sso && licenseFeatures.saml; +}; +``` + +**Used by:** +- SAML SSO +- Pretty URLs (slug feature) +- Domain/Organization settings page + +--- + +## 3. Files Using Enterprise Features + +### 3.1 Core License/Feature Check Files + +| File | Purpose | +|------|---------| +| `apps/web/modules/ee/license-check/lib/license.ts` | License fetching & caching | +| `apps/web/modules/ee/license-check/lib/utils.ts` | Permission check functions | +| `apps/web/modules/ee/license-check/types/enterprise-license.ts` | Type definitions | +| `apps/web/lib/constants.ts` | `IS_FORMBRICKS_CLOUD`, `ENTERPRISE_LICENSE_KEY` | + +### 3.2 Feature-Specific Implementation Files + +#### Remove Branding +- `apps/web/modules/ee/whitelabel/remove-branding/actions.ts` +- `apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx` +- `apps/web/modules/projects/settings/look/page.tsx` +- `apps/web/modules/projects/settings/actions.ts` + +#### Whitelabel / Email Customization +- `apps/web/modules/ee/whitelabel/email-customization/actions.ts` +- `apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx` +- `apps/web/app/(app)/environments/[environmentId]/settings/(organization)/domain/page.tsx` + +#### Multi-Language Surveys +- `apps/web/modules/ee/multi-language-surveys/lib/actions.ts` +- `apps/web/modules/ee/multi-language-surveys/components/*.tsx` +- `apps/web/modules/ee/languages/page.tsx` + +#### Contacts & Segments +- `apps/web/modules/ee/contacts/segments/actions.ts` +- `apps/web/modules/ee/contacts/page.tsx` +- `apps/web/modules/ee/contacts/api/v1/**/*.ts` +- `apps/web/modules/ee/contacts/api/v2/**/*.ts` + +#### Teams & Access Control +- `apps/web/modules/ee/teams/team-list/components/teams-view.tsx` +- `apps/web/modules/ee/role-management/actions.ts` +- `apps/web/modules/organization/settings/teams/page.tsx` +- `apps/web/modules/organization/settings/teams/actions.ts` + +#### SSO / SAML +- `apps/web/modules/ee/sso/lib/sso-handlers.ts` +- `apps/web/modules/ee/auth/saml/api/**/*.ts` +- `apps/web/modules/ee/auth/saml/lib/*.ts` +- `apps/web/modules/auth/lib/authOptions.ts` + +#### Two-Factor Authentication +- `apps/web/modules/ee/two-factor-auth/actions.ts` +- `apps/web/modules/ee/two-factor-auth/components/*.tsx` + +#### Quotas +- `apps/web/modules/ee/quotas/actions.ts` +- `apps/web/modules/ee/quotas/components/*.tsx` +- `apps/web/modules/ee/quotas/lib/*.ts` + +#### Audit Logs +- `apps/web/modules/ee/audit-logs/lib/handler.ts` +- `apps/web/modules/ee/audit-logs/lib/service.ts` + +#### Billing (Cloud Only) +- `apps/web/modules/ee/billing/page.tsx` +- `apps/web/modules/ee/billing/api/lib/*.ts` +- `apps/web/modules/ee/billing/components/*.tsx` + +### 3.3 API Routes Using Feature Checks + +| Route | Feature Check | +|-------|---------------| +| `apps/web/app/api/v1/client/[environmentId]/responses/route.ts` | Spam protection | +| `apps/web/app/api/v2/client/[environmentId]/responses/route.ts` | Spam protection | +| `apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts` | Cloud limits | +| `apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/route.ts` | Contacts | +| `apps/web/modules/api/v2/management/responses/lib/response.ts` | Cloud limits | + +### 3.4 UI Pages with Conditional Rendering + +| Page | Condition | +|------|-----------| +| `apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/` | Cloud only | +| `apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx` | On-premise only | +| `apps/web/app/(app)/environments/[environmentId]/settings/(organization)/domain/page.tsx` | On-premise only | +| `apps/web/app/p/[slug]/page.tsx` (Pretty URLs) | On-premise only | + +--- + +## 4. Configuration & Environment Variables + +### 4.1 Key Environment Variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `IS_FORMBRICKS_CLOUD` | Enables cloud mode | `"0"` | +| `ENTERPRISE_LICENSE_KEY` | License key for on-premise | (empty) | +| `STRIPE_SECRET_KEY` | Stripe API key (Cloud) | (empty) | +| `AUDIT_LOG_ENABLED` | Enable audit logs | `"0"` | +| `SAML_DATABASE_URL` | SAML configuration DB | (empty) | + +### 4.2 Database Schema + +```prisma +// Organization billing stored in JSON column +billing: { + stripeCustomerId: string | null, + plan: "free" | "startup" | "scale" | "enterprise", + period: "monthly" | "yearly", + limits: { + projects: number | null, + monthly: { + responses: number | null, + miu: number | null, + } + }, + periodStart: Date | null +} +``` + +--- + +## 5. Problems with Current Approach + +### 5.1 Code Duplication + +Almost every feature check function has this pattern: +```typescript +if (IS_FORMBRICKS_CLOUD) { + // Check billing plan +} else { + // Check license feature +} +``` + +This is repeated in: +- 8+ permission check functions in `utils.ts` +- 30+ files that consume these functions +- Multiple API routes and pages + +### 5.2 Inconsistent Feature Gating + +| Feature | Cloud Gating | On-Premise Gating | +|---------|--------------|-------------------| +| Remove Branding | `plan !== FREE` | `license.features.removeBranding` | +| Multi-Language | `plan === CUSTOM` OR `license.multiLanguageSurveys` | `license.multiLanguageSurveys` | +| Follow-ups | `plan === CUSTOM` | Always allowed | +| SAML SSO | Never allowed | `license.sso && license.saml` | +| Teams | `plan === CUSTOM` OR `license.accessControl` | `license.accessControl` | + +### 5.3 Confusing License Requirement on Cloud + +Cloud deployments still require `ENTERPRISE_LICENSE_KEY` to be set for enterprise features to work: +```typescript +// utils.ts - getFeaturePermission +if (IS_FORMBRICKS_CLOUD) { + return license.active && billingPlan !== PROJECT_FEATURE_KEYS.FREE; + // ^^^^^^^^^^^^^^ Still checks license! +} +``` + +This means Cloud needs **both**: +1. Active billing plan (Stripe subscription) +2. Active enterprise license + +### 5.4 Fallback Logic Complexity + +```typescript +const featureFlagFallback = async (billingPlan) => { + const license = await getEnterpriseLicense(); + if (IS_FORMBRICKS_CLOUD) return license.active && billingPlan === PROJECT_FEATURE_KEYS.CUSTOM; + else if (!IS_FORMBRICKS_CLOUD) return license.active; + return false; +}; +``` + +Features have "fallback" behavior for backwards compatibility, adding another layer of complexity. + +### 5.5 Testing Complexity + +Tests must mock both: +- `IS_FORMBRICKS_CLOUD` constant +- `getEnterpriseLicense()` function +- `organization.billing.plan` in some cases + +See: `apps/web/modules/ee/license-check/lib/utils.test.ts` (400+ lines of test mocking) + +--- + +## 6. Feature Availability Matrix + +| Feature | Free (Cloud) | Startup (Cloud) | Custom (Cloud) | No License (On-Prem) | License (On-Prem) | +|---------|--------------|-----------------|----------------|---------------------|-------------------| +| Remove Branding | ❌ | ✅ | ✅ | ❌ | ✅* | +| Whitelabel | ❌ | ✅ | ✅ | ❌ | ✅* | +| Multi-Language | ❌ | ❌ | ✅ | ❌ | ✅* | +| Teams & Roles | ❌ | ❌ | ✅ | ❌ | ✅* | +| Contacts | ❌ | ❌ | ❌ | ❌ | ✅* | +| SSO (OIDC) | ❌ | ❌ | ❌ | ❌ | ✅* | +| SAML SSO | ❌ | ❌ | ❌ | ❌ | ✅* | +| 2FA | ❌ | ❌ | ❌ | ❌ | ✅* | +| Audit Logs | ❌ | ❌ | ❌ | ❌ | ✅* | +| Quotas | ❌ | ❌ | ✅ | ❌ | ✅* | +| Spam Protection | ❌ | ❌ | ✅ | ❌ | ✅* | +| Follow-ups | ❌ | ❌ | ✅ | ✅ | ✅ | +| Pretty URLs | ❌ | ❌ | ❌ | ✅ | ✅ | +| Projects Limit | 3 | 3 | Custom | 3 | Custom* | + +*Depends on specific license feature flags + +--- + +## 7. Recommendations for Refactoring + +### 7.1 Unified Feature Access Layer + +Create a single `FeatureAccess` service that abstracts the deployment type: + +```typescript +interface FeatureAccessService { + canAccessFeature(feature: FeatureKey, context: AccessContext): Promise; + getLimit(limit: LimitKey, context: LimitContext): Promise; +} +``` + +### 7.2 Normalize Feature Flags + +Both Cloud and On-Premise should use the same feature flag schema. Cloud billing plans should map to predefined feature sets. + +### 7.3 Remove License Requirement from Cloud + +Cloud should not need `ENTERPRISE_LICENSE_KEY`. The license server should be bypassed entirely, with features controlled by billing plan. + +### 7.4 Consider Feature Entitlements + +Move to an "entitlements" model where: +- Cloud: Stripe subscription metadata defines entitlements +- On-Premise: License API returns entitlements + +Both resolve to the same `TFeatureEntitlements` type. + +--- + +## 8. Files That Would Need Changes + +### High Priority (Core Logic) +1. `apps/web/modules/ee/license-check/lib/license.ts` +2. `apps/web/modules/ee/license-check/lib/utils.ts` +3. `apps/web/lib/constants.ts` + +### Medium Priority (Feature Implementations) +4. All files in `apps/web/modules/ee/*/actions.ts` +5. `apps/web/modules/environments/lib/utils.ts` +6. `apps/web/modules/survey/lib/permission.ts` +7. `apps/web/modules/survey/follow-ups/lib/utils.ts` + +### Lower Priority (UI Conditional Rendering) +8. Settings pages with `IS_FORMBRICKS_CLOUD` checks +9. `UpgradePrompt` component usages +10. Navigation components + +--- + +## 9. Summary + +The current implementation has **organic complexity** from evolving independently for Cloud and On-Premise deployments. A refactor should: + +1. **Unify** the feature access mechanism behind a single interface +2. **Simplify** by removing the dual-check pattern +3. **Normalize** feature definitions across deployment types +4. **Test** with a cleaner mocking strategy + +This would reduce the 100+ files touching enterprise features to a single source of truth, making the codebase more maintainable and reducing bugs from inconsistent feature gating. diff --git a/ENTERPRISE_PRD.md b/ENTERPRISE_PRD.md new file mode 100644 index 0000000000..e39d1fbaa4 --- /dev/null +++ b/ENTERPRISE_PRD.md @@ -0,0 +1,428 @@ +I've spent ~2 days iterating over this, setting up Stripe, building our update pricing table, etc. So even though the formatting suggests this to be AI Slob, it's hand-crafted and I've read every line to make sure there is no misleading information 😇 + +------ + +### Unified Billing & Feature Access + +**Document Version:** 2.1 +**Last Updated:** January 17, 2026 +**Status:** Ready for development + +--- + +## 1. Executive Summary + +Formbricks Cloud needs a unified, Stripe-native approach to billing, feature entitlements, and usage metering. The current implementation has billing logic scattered throughout the codebase, making it difficult to maintain pricing consistency and add new features. + +This PRD outlines the requirements for: +1. Using Stripe as the single source of truth for features and billing +2. Implementing usage-based billing with graduated pricing +3. Giving customers control through spending caps + +**Scope**: This initiative focuses on Formbricks Cloud. On-Premise licensing will be addressed separately. + +--- + +## 2. Problem Statement + +### Current Pain Points + +1. **Scattered Billing Logic**: Feature availability is determined by code checks against `organization.billing.plan`, requiring code changes for any pricing adjustment. + +2. **Inconsistent Feature Gating**: Different features use different patterns to check access, making it unclear what's available on each plan. + +3. **No Usage-Based Billing**: Current plans have hard limits. Customers hitting limits must upgrade to a higher tier even if they only need slightly more capacity. + +4. **No Spending Controls**: Customers on usage-based plans have no way to cap their spending. + +5. **Manual Usage Tracking**: Response and user counts are tracked locally without integration to billing. + +--- + +## 3. Goals + +1. **Stripe as Source of Truth**: All feature entitlements and pricing come from Stripe, not hardcoded in the application. + +2. **Usage-Based Billing**: Implement graduated pricing where customers pay for what they use beyond included amounts. + +3. **Customer Control**: Allow customers to set spending caps to avoid unexpected charges. + +4. **Proactive Communication**: Notify customers as they approach usage limits. + +--- + +## 4. Feature Requirements + +### 4.1 Stripe as Single Source of Truth + +**Requirement**: The Formbricks instance should not contain billing or pricing logic. All feature availability must be determined by querying Stripe. + +**What This Means**: +- No hardcoded plan names or feature mappings in the codebase +- No `if (plan === 'pro')` style checks +- Feature checks query Stripe Entitlements API +- Pricing displayed in UI is fetched from Stripe Products/Prices +- Plan changes take effect immediately via Stripe webhooks + +**Benefits**: +- Change pricing without code deployment +- Add new plans without code changes +- A/B test pricing externally +- Single source of truth for sales, support, and product + +--- + +### 4.2 Stripe Entitlements for Feature Access + +**Requirement**: Use Stripe's Entitlements API to determine which features each customer can access. + +**How It Works**: +1. Define Features in Stripe (see inventory below) +2. Attach Features to Products via ProductFeature +3. When customer subscribes, Stripe creates Active Entitlements +4. Application checks entitlements before enabling features +5. Stripe is already setup correctly with all Products & Features ✅ + +**Multi-Item Subscriptions Simplify Entitlements**: +- Each plan subscription includes multiple prices (flat fee + metered usage) on the **same Product** +- Since all prices belong to one Product, calling `stripe.entitlements.activeEntitlements.list()` returns all features for that plan automatically +- No need to check multiple products or stitch together entitlements from separate subscriptions + +**Feature Inventory (not up-to-date but you get the idea)**: + +| Feature Name | Lookup Key | Description | +|--------------|------------|-------------| +| Hide Branding | `hide-branding` | Hide "Powered by Formbricks" | +| API Access | `api-access` | Gates API key generation & API page access | +| Integrations | `integrations` | Gates integrations page & configuration | +| Custom Webhooks | `webhooks` | Webhook integrations | +| Email Follow-ups | `follow-ups` | Automated email follow-ups | +| Custom Links in Surveys | `custom-links-in-surveys` | Custom links within surveys | +| Custom Redirect URL | `custom-redirect-url` | Custom thank-you redirects | +| Two Factor Auth | `two-fa` | 2FA for user accounts | +| Contacts & Segments | `contacts` | Contact management & segmentation | +| Teams & Access Roles | `rbac` | Team-based permissions | +| Quota Management | `quota-management` | Response quota controls | +| Spam Protection | `spam-protection` | reCAPTCHA integration | +| Workspace Limit 1 | `workspace-limit-1` | Up to 1 workspaces | +| Workspace Limit 3 | `workspace-limit-3` | Up to 3 workspaces | +| Workspace Limit 5 | `workspace-limit-5` | Up to 5 workspaces | + +Image + +--- + +### 4.3 Plan Structure + +**Plans & Pricing**: + +| Plan | Monthly Price | Annual Price | Savings | +|------|---------------|--------------|---------| +| **Hobby** | Free | Free | — | +| **Pro** | $89/month | $890/year | 2 months free | +| **Scale** | $390/month | $3,900/year | 2 months free | + +**Usage Limits**: + +| Plan | Workspaces | Responses/mo | Contacts/mo | Overage Billing | +|------|------------|--------------|-------------|-----------------| +| **Hobby** | 1 | 250 | — | No | +| **Pro** | 3 | 2,000 | 5,000 | Yes | +| **Scale** | 5 | 5,000 | 10,000 | Yes | + +**Note**: Hobby plan does not include Respondent Identification or Contact Management. Overage billing is only available on Pro and Scale plans. + +Image + +--- + +### 4.4 Restricted Features (Hobby & Trial Exclusions) + +**Requirement**: Certain high-risk features must be excluded from Free (Hobby) plan AND Trial users to prevent fraud and abuse. Other features are included in Trial to maximize conversion. + +**Restricted Features (blocked from Hobby + Trial)**: + +| Feature | Lookup Key | Abuse Risk | Why Restricted | +|---------|------------|------------|----------------| +| Custom Redirect URL | `custom-redirect-url` | High | Phishing redirects after survey | +| Custom Links in Surveys | `custom-links-in-surveys` | High | Malicious link distribution in survey content | + +**Trial-Included Features (to drive conversion)**: + +| Feature | Lookup Key | Why Included in Trial | +|---------|------------|----------------------| +| Webhooks | `webhooks` | Low abuse risk, high setup effort = conversion driver | +| API Access | `api-access` | Low abuse risk, high integration value | +| Integrations | `integrations` | Low abuse risk, high integration value | +| Email Follow-ups | `follow-ups` | Requires email verification, monitored | +| Hide Branding | `hide-branding` | No abuse risk, strong conversion driver | +| RBAC | `rbac` | No abuse risk, team adoption driver | +| Spam Protection | `spam-protection` | Actually prevents abuse | +| Quota Management | `quota-management` | Administrative feature | + +**Implementation**: +- Restricted features are NOT attached to Hobby or Trial products in Stripe +- Trial includes most Pro/Scale features to maximize value demonstration +- Application checks entitlements via Stripe API - if feature not present, show existing upgrade UI + +--- + +### 4.5 Usage-Based Billing with Graduated Pricing + +Image + +**Requirement**: Implement usage-based billing where customers pay a base fee that includes a usage allowance, with flat overage pricing. + +**Metrics to Meter**: + +| Metric | Event Name | Description | +|--------|------------|-------------| +| **Responses** | `response_created` | Survey responses submitted | +| **Identified Contacts** | `unique_contact_identified` | Unique contacts identified per month | + +**Identified Contacts Definition (ON HOLD)**: +An identified contact is one that has been identified in the current billing period via: +- SDK call: `formbricks.setUserId(userId)` +- Personal Survey Link access +- This OUT OF SCOPE for the first iteration to not become a blocker. We can add it if all works end-to-end + +**Counting Rules**: +- Each contact identification counts (even if same contact identified multiple times via different methods) +- Same contact re-accessing their personal link = 1 count (same contact) +- Billing period is monthly (even for annual subscribers) +- Meter events sent immediately (real-time) + +**Hard Limits via Stripe Metering**: +- Usage is metered through Stripe for billing AND enforcement +- When included usage is exhausted, overage rates apply +- No separate local limit enforcement needed + +--- + +### 4.6 Spending Caps + +**Requirement**: Customers must be able to set a maximum monthly spend for usage-based charges. + +**Behavior**: + +| Cap Setting | Effect | +|-------------|--------| +| No cap (default) | Usage billed without limit | +| Cap with "Warn" | Notifications sent, billing continues | +| Cap with "Pause" | Surveys paused when cap reached | + +**Configuration**: +- Minimum spending cap: **$10** +- No grace period when cap is hit +- Immediate pause if "Pause" mode selected +- Stripe does not provide spending caps out of the box, this is something we need to custom develop + +**When Cap is Reached (Pause mode)**: +- All surveys for the organization stop collecting responses (needs to be implemented) +- Existing responses are preserved +- In-app banner explains the pause +- Email notification sent to billing contacts +- Owner can lift pause or increase cap + +Image + +_The Pause vs. Alert mode is missing so far._ + +--- + +### 4.7 Usage Alerts via Stripe Meter Alerts + +**Requirement**: Proactively notify customers as they approach their included usage limits. + +**Alert Thresholds**: + +| Threshold | Notification | +|-----------|--------------| +| 80% of included | Email notification | +| 90% of included | Email + in-app banner | +| 100% of included | Email + in-app + (if cap) action | + +**Notification Content**: +- Current usage vs included amount +- What happens next (overage pricing applies) +- Link to upgrade or adjust spending cap + +Image + +--- + +### 4.8 Annual Billing with Monthly Limits + +**Requirement**: Support annual payment option while keeping all usage limits monthly. + +**Behavior**: +- Annual subscribers pay upfront for 12 months +- **2 months free** discount (annual = 10x monthly price) +- Usage limits reset monthly (same as monthly subscribers) +- Overage is billed monthly +- Example: Annual Pro pays $890/year, gets 2,000 responses/month every month + +Image + +--- + +### 4.9 Reverse Trial Experience + +**Requirement**: New users should experience premium features immediately through a Reverse Trial model. + +**Trigger**: We have UI to present to them to opt into the free trial + +**Trial Terms**: +- Duration: 14 days +- Features: Enroll to Trial Product (free) +- Limits: We have to see how to enforce those, gotta check what Stripe API offers us. Probably a dedicated Trial Meter +- No payment required to start +- Stripe customer created immediately (for metering) + +**Post-Trial (No Conversion)**: +- Downgrade to Hobby (Free) tier immediately +- Pro features disabled immediately +- Data preserved but locked behind upgrade + +--- + +### 4.10 Stripe Customer Creation on Signup + +**Requirement**: Create a Stripe customer immediately when a new organization is created. + +**Rationale**: +- Enables usage metering from day one +- Stripe handles hard limits via metering +- Simplifies upgrade flow (customer already exists) + +**What Gets Created**: +- Stripe Customer with organization ID in metadata +- No subscription (Hobby tier has no subscription) +- No payment method (added on first upgrade) + +--- + +### 5.1 Subscription Architecture: Multi-Item Subscriptions + +**Key Insight**: Each plan uses a **Subscription with Multiple Items** — one flat-fee price and metered usage prices, all belonging to the same Product. This allows us to charge for the base plan, meter and charge per used item. + +**How It Works**: + +```javascript +// Creating a Pro subscription with flat fee + usage metering +const subscription = await stripe.subscriptions.create({ + customer: 'cus_12345', + items: [ + { price: 'price_pro_monthly' }, // $89/mo flat fee + { price: 'price_pro_responses_usage' }, // Metered responses + { price: 'price_pro_contacts_usage' }, // Metered contacts + ], +}); +``` + +**Why This Matters for Entitlements**: +- All prices belong to the **same Product** (e.g., `prod_ProPlan`) +- Stripe Entitlements API automatically returns all features attached to that Product +- No need to check multiple products or subscriptions +- Single source of truth for feature access + +**What Customers See** (Single Invoice): + +| Description | Qty | Amount | +|-------------|-----|--------| +| Pro Plan (Jan 1 - Feb 1) | 1 | $89.00 | +| Pro Plan - Responses (Jan 1 - Feb 1) | 1,500 (First 1,000 included) | $40.00 | +| Pro Plan - Contacts (Jan 1 - Feb 1) | 2,500 (All included) | $0.00 | +| **Total** | | **$129.00** | + +### 5.2 Products & Prices + +Each plan Product contains multiple Prices: + +| Product | Stripe ID | Prices | +|---------|-----------|--------| +| **Hobby Tier** | `prod_ToYKB5ESOMZZk5` | Free (no subscription required) | +| **Pro Tier** | `prod_ToYKQ8WxS3ecgf` | `price_pro_monthly` ($89), `price_pro_yearly` ($890), `price_pro_usage_responses`, `price_pro_usage_contacts` | +| **Scale Tier** | `prod_ToYLW5uCQTMa6v` | `price_scale_monthly` ($390), `price_scale_yearly` ($3,900), `price_scale_usage_responses`, `price_scale_usage_contacts` | +| **Trial Tier** | `prod_TodVcJiEnK5ABK` | `price_trial_free` ($0), metered prices for tracking | + +**Note**: Response and Contact metered prices use **graduated tiers** where the first N units are included (priced at $0), then overage rates apply. + + +--- + +## 6. Non-Functional Requirements + +### 6.1 Performance + +- Entitlement checks: <100ms (p50), <200ms (p99) with caching +- Usage metering: Non-blocking, immediate send +- Spending cap checks: <50ms + +### 6.2 Reliability + +- Stripe unavailable: Use cached entitlements (max 5 min stale) +- Meter event fails: Queue for retry (at-least-once delivery) +- Webhook missed: Entitlements auto-refresh on access + +### 6.3 Data Consistency + +- Stripe is source of truth +- Local `organization.billing` is a cache only +- Cache invalidated via webhooks + +--- + +## 7. Migration Considerations + +### Existing Customers with Custom Limits + +**Problem**: Some existing customers have negotiated custom limits that don't fit the new plan structure. + +**Approach**: Grandfather indefinitely on legacy pricing until they choose to migrate. + +- Existing customers keep their current plans and limits +- No forced migration +- New billing system only applies to new signups and customers who voluntarily upgrade/change plans +- Legacy customers get a simplified view with the new usage meters UI and a the "Manage subscription" button. We hide all of the other UI and prompt them to reach out to Support to change their pricing (Alert component) + +--- + +## 8. Key Decisions + +| Topic | Decision | +|-------|----------| +| Free tier metering | Use Stripe for hard limits (no local enforcement) | +| Annual discount | 2 months free | +| Minimum spending cap | $10 | +| Cap grace period | None (immediate) | +| Contact identification counting | Each identification counts | +| Personal link re-access | Same contact = 1 count | +| Downgrade behavior for restricted features | Immediate disable | +| Meter event timing | Immediate (real-time) | +| Currency | USD only | +| Default spending cap | No cap | +| Overage visibility | Billing page | +| Migration for custom limits | Grandfather indefinitely | + +--- + +## 9. Out of Scope + +1. **On-Premise licensing**: Will be addressed separately +2. **Self-serve downgrade**: Handled via Stripe Customer Portal +3. **Refunds**: Handled via Stripe Dashboard +4. **Tax calculation**: Handled by Stripe Tax +5. **Invoice customization**: Handled via Stripe settings + +--- + +## 10. Setting up + +**Stripe** Sandbox Cloud: Dev + +Image + +In this branch you find; +- All of the dummy UI screenshotted here. Make sure to clean up after it was successfully implemented (has dummy UI code) +- A comprehensive analysis of our current, inconsistent feature flagging called ENTERPRISE_FEATURE_ANALYSIS \ No newline at end of file diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/components/select-plan-onboarding.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/components/select-plan-onboarding.tsx new file mode 100644 index 0000000000..07e23920d1 --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/components/select-plan-onboarding.tsx @@ -0,0 +1,20 @@ +import { SelectPlanCard } from "@/modules/ee/billing/components/select-plan-card"; +import { Header } from "@/modules/ui/components/header"; + +interface SelectPlanOnboardingProps { + organizationId: string; +} + +export const SelectPlanOnboarding = ({ organizationId }: SelectPlanOnboardingProps) => { + const nextUrl = `/organizations/${organizationId}/workspaces/new/mode`; + + return ( +
+
+ +
+ ); +}; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/page.tsx new file mode 100644 index 0000000000..d658317618 --- /dev/null +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/plan/page.tsx @@ -0,0 +1,29 @@ +import { redirect } from "next/navigation"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getOrganizationAuth } from "@/modules/organization/lib/utils"; +import { SelectPlanOnboarding } from "./components/select-plan-onboarding"; + +interface PlanPageProps { + params: Promise<{ + organizationId: string; + }>; +} + +const Page = async (props: PlanPageProps) => { + const params = await props.params; + + // Only show on Cloud + if (!IS_FORMBRICKS_CLOUD) { + return redirect(`/organizations/${params.organizationId}/workspaces/new/mode`); + } + + const { session } = await getOrganizationAuth(params.organizationId); + + if (!session?.user) { + return redirect(`/auth/login`); + } + + return ; +}; + +export default Page; diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 7b507034a1..8d7ac09ca3 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -2,6 +2,7 @@ import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import ClientEnvironmentRedirect from "@/app/ClientEnvironmentRedirect"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getIsFreshInstance } from "@/lib/instance/service"; import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; import { getAccessFlags } from "@/lib/membership/utils"; @@ -66,6 +67,10 @@ const Page = async () => { if (!firstProductionEnvironmentId) { if (isOwner || isManager) { + // On Cloud, show plan selection first for new users without any projects + if (IS_FORMBRICKS_CLOUD) { + return redirect(`/organizations/${userOrganizations[0].id}/workspaces/new/plan`); + } return redirect(`/organizations/${userOrganizations[0].id}/workspaces/new/mode`); } else { return redirect(`/organizations/${userOrganizations[0].id}/landing`); diff --git a/apps/web/images/customer-logos/cal-logo-light.svg b/apps/web/images/customer-logos/cal-logo-light.svg new file mode 100644 index 0000000000..bccfe813ef --- /dev/null +++ b/apps/web/images/customer-logos/cal-logo-light.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/images/customer-logos/ethereum-logo.png b/apps/web/images/customer-logos/ethereum-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3b4575d984c777e7bb7f5095b808446a6d7919ce GIT binary patch literal 7240 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 + + + + + + + + diff --git a/apps/web/images/customer-logos/github-logo.png b/apps/web/images/customer-logos/github-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..adc1552ba8b803728c85be52bcf181e217720455 GIT binary patch literal 27651 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 Bm>2*6 literal 0 HcmV?d00001 diff --git a/apps/web/images/customer-logos/pigment-logo.webp b/apps/web/images/customer-logos/pigment-logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..0ab6efdd367ad3c7b3c219e9cb523cd984103f19 GIT binary patch literal 12154 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 X#&6yyAF}dS$!?L;T>6jkfA;?ey0vs> literal 0 HcmV?d00001 diff --git a/apps/web/images/customer-logos/siemens.png b/apps/web/images/customer-logos/siemens.png new file mode 100644 index 0000000000000000000000000000000000000000..2cbb825948cf6829ab7edf3566e72f67dfb9070c GIT binary patch literal 7797 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 literal 0 HcmV?d00001 diff --git a/apps/web/images/customer-logos/university-of-copenhegen.png b/apps/web/images/customer-logos/university-of-copenhegen.png new file mode 100644 index 0000000000000000000000000000000000000000..cda523057c6d2ef70157563ef474e3e71cc07ede GIT binary patch literal 54140 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%0A}S zC<>IMnaf{q?k}#H)xZ0^R{4P7F5T}FzdXqiu9<4Lwgsg+0o!tRMnzx(`*xcqayFM0MnGMNlcYnhJ4u_)^60f|Uj?~X9N)&>HF3LyyRP3s%? zwJfslj3>uJ_`g|RqzO9SRnb`Ao*3^fyTil!>UWMB!y_u#9{+qUnlCNG9c>vTjmCj9)poX>4$9M5MsH`!Wy z0jDFNdD&-s+zIAr30cCqwbW_5z~o@2zayB$7~sm4zDxJY--yx%V>WhA_m;Y9F5N|5KoRI(}+?Xk$)cF!YVnM#OlpAL6K>mmNmTRCcqF)nN5IV z?e!$qO2;HISv)wBSUDr)3)5)o-9G{`8+Bc>2I~UaJkt1|2e^PCVm=rI1av8m8_p#* z*CUgg@fKoEXV1$n7vA`i_2o+>dJmqK(n<*SVjbqyJ^$9C6DiDU3+|KxF@N9h5w187 zVhJ@RRj8RO4xW3E7fqPxK*QxPo4M~<)Bx1CT$~91L914_iD$q>-DSaWDmx)$HAs~n z)$H4>>RayF+#jB`kSgl)_dd*HL1St6e# zgU*R>u~X{znrR5PWNODTbfYqZ1Xg)7}wl%sT!PZt6B>9hdHP^ADF ze~KYZZxd~15oNSzpOX7Uz>WcT-p)#KG}WmtWT}c+U|_%Ow{GLjUYs^e&Jf7)DiKLp z{`Np&zGCt`(Jt}&EtPsPB{MCOnFGsJ*1Iq^Hci1>9tHGJOXds$RMmVX|APnk(E}HN zBIWhB!LYYH=|R`62;6%W><=^GH6JhVQI;g$by9WpK#70GvEQYiG{%tphwzJH+6~;! z@2}DZZS&@@p$MTRI$QNUZNgiN-leYB_T1+7)2T=O3vlE;ZJ^fzXZZ3(v2FR(zw_jE znNfhL7r46%n%Mc%Z8s(=CJbtAePGlUyqU1Nt6zm3c?s9tbSn&&-IlE=oy+AoOd}Mt z9(AVC4o1C;#o>Y_L^Ag1pD*`Ux3&F7V zFI)C2^yTiBo(r`mKRwT?wOr1XS@5a(*)p4N!;fAB`$VeXOnvu`B_0=K5->=VjnTii z4=!=425c8V;W;96IKXG)bD`nzG?%BE=C>D$JenH;SPyjKs_t=?W#Me>t|eV%+QQ9M z=tXvx`Q-%mg*J0!Mi?&Oh&E^t@-bE6d^Rb2Y(8D!{LHFUZKaZu*v}>4!BW3k{oDHi z^?Q4J!dHJy-Zcxr@_Zy=VIPZ9^37&-6|5E_lem_}mrk)0#6;_SZy+cFO_44TL4Qlu(@O?{Pn?byfrROj zu&)XVK;o)0ZTJ|9rk^x#7LakwKHn|cA)zt0l3?ie61_fh)c{Bh+nnoJV)ALP8S5YD zT|L|FqdX|j9+`UCd1ZwN{*Bc zlqAVo-)yL`$A81kPXBE5(DTb~F=)^hisb!SerWoVq;p*5AC5TV*cjCV?;MXmPOuX! zEk^naF}}AMRDV~fmVL;kaGw6)xx;bpL|?4+%<9D=51<7%Fe}vbS!4{c?S#`my;nfM zfJ(o0y`*H%MM@eBZMSY%siZ0V=;IicH%5xDd9^1Cs3;Yf*#HOq@k2S7OE;_%yBDIN zUgkJ^c#*)HSXW~+(BxuZ^4@!E@{7#HC;vf4?c*Ys=ReDAWwpFp=! zZ$jWJt>hR$S)a^92vLZ%8&EtAGc`*4x6p;QcA|0??jPOKkL^x>WCB@*G1g#6>|06} z&dkS|9rM4FG3)+&?fa^W=@Av2z-U4)cT0_YO6BWlVU5Z%fA>o@|fl~TB47$`L1NlDAyZWp{ zt$#eL53?sq6MIv!3*=JGK?XE%u!re{ouO(){d_(h*z!7P@a?g5(ao;M=>%xt8Uo%g zEYt|`ovV79gXR~WfVah6;fCmJ+d6FJDeU&Cei(+w_ro z*}D$nsgVo(-3IyKc)bke&LB6+<((DB8JPU}O8C3&q4SrR#s*}3d5{dizgNoZwV6Re zrr}Q9yIf+Am&^%5Mfm>x7x%o`qwIELR81@8Uy(Q_?@BPU%Ydv-SpsRC?V~QO1`GbR z!wLD)xez$@$3U!zlDo;gsl4q>7ZcNsRMKxAe-)k3JcxZKXXYMl!S%bWTy80V48o?rMz_gZL zt_i1=OIo3&X^)Fn2%M`1QOnBZ0E|b&Tp1^DdC6^Xu`_!=WBa$UO=pC1eL7c$`=Shl=gsy#og*r+t2WX zfu@x8`#{-K7{@;hP~_|tRGD~E#e5u*5*uBzlVo281S)y$;UFH)uhY`JwOSTTLLmvs zQIi1xKa`;7%4|y_AI`sVX5Ae!S9YummLWQJU!wGfvo~+=I2Y0$1zdeeKyrfW6f76~ zwS4M60$&sG^}XcUtZv>9uVyR}3)FA5KVBr>5u+X-1+!3Ncp4fO$P9WA%!YOBX7}39 zS_9vS&vfYhSX5g9Ec~p8US?Yb;7M^bxSX=rkz--yf2xh3@&bX~c3V{~6o(&l73=lhewD9TX2PhCwn)HKvJ zhg!D1y40Rc>>P|QnNRr=edn*LT>(gHn0aUwv zY;1#w_kaoUXMBwKrv*~%d{{bhVfV`~*NNsWcSWU&6BWVnv&cxG=adZ~JDmqm-~?Kc zf=dIuwz>3AMZBD?%_3gitJ6*EGD7T&fYec^w-6Jh5KZFO3DuUvG33sA4waFY*>PgQ zqpLZ1AgEWY1iFxvM=Vk0`KdTIA>c`juj$l~F9PJwQ-B%GWlmBB-V=bU~~JkQAMm9j}s)Bfz~$X+dZ z=N$2^+3_qm(!v@c36^@)l|(mf)hb*0yS?~e_!j-Q&cMT9oLeA8DO_#;hi|>X%+L~_ z`^T$BV4~D&;KSI@5?8$u_(LoVPW5Q^sMxBWPMj-5rD-iCUkxH`iU1hbR;Y3C?#Uly z0JX@FkoFwtxf%hE8j2ws99$fy7d`7(ltJ9TFlSU(f*ZNPsl~~g@Zav2zIxcX!kqDE(51p0+eaM_+u%n(7SL4Kjp(G?_$mF| zJUbd;YWnme&P9pNlO`-2-fjRE|3X*vG8WCFDhLL1+Jq=trLhO0@L9+Q-$(taNh2kumksd z3H*n@BCl^~3AHI%pPkr^tpBt{<(SI_bsD!zNd8&k-4~R`XL>G^@$d=1xSZTKd6U4KpghP+8l9*(Bz1vF+F?`;TW zgA%!KvldK7pzi;tgCKn`G7?c@j(sf*j{o*CoWK#NZY(0=@XHCBxJaMpG+!?c(Shrc z)Mbh8jNqlyl@C{P;o)3VjRQ7aFTd3OPjc?-&PX2!&9@goXcOkB{yh*Up^;Uf;%@lE zgStPS6CCgZ!(}9;`w|jf?y8A+AQ_lp1c$!oTxpi1)I$|rJm5MK2iQj(w)?5}M zU)mbB1%A$=o=(?hbKl|cnGKP#Q^q+TH|>vif*-5TBGOkXO&LOMCc}0lQ8ig^H7l+T z&l3kT^J(3sr8g+*m-Dvlvv`QP4_!JKUa#@#DEK!Xy>AhN*Fg(bK#z=AgZ*Phw&rX4u0aZzt zlQjDOBDGy=qDnDSK>NeNJ0Fb;7oYkqqNj`{#LxWWK)_RgjuyD7&O9g3%)SHf401xI zJC0DQ6)WXx3p4~ETxf8TdkMhnLC~3%OZMXl;B-4xqDwxQ1^nL= zf&+uLxg_M?X3iiZCH|?RFpR^e<;+*;_PG3Dz8vupl>h@DHNBzv{w?Z9333-}P3g$^I*0R0_MfIm8VtM4@!>GJ=9 z{a^0gA@)Pndm{M4)a3-!V$0K#cRupYc0i2dIipammaJUl6Wr5t)>gQAKk5s*yt^>n zOn7?C*#>_q=1+|d-2(fS)Du31i2@l$ugSyL$6r04(A64O;fSV;qNW~$N-P4pdqy+# zg@n!aH)!+(NCX7aJ3{rUrU^_pJDSl{{rmC1YbMf^gDBcpXB4wj!#Dr zs=xUnjYzk2hA`39e$``l-0jEVhOxdVsQ zJswrJ-fX+SNu%8dI-#IwJePjnIpI?_5NDGz#2f+Qc#XtBHch`98c2 zm#mS$K^ZM@P~if6e!kpcGAn2r@`#zDYV5PX7ZH)mP4_LDkn@&UaFd#TW3@OrU$HXC zY#vY7c5T8B)~A$Ext@~o9<^)Hl<3z$gPv-$^~$-j46i&wknzE%SARdB5>_ekBE zn$MZDbmMQVl{`)6EOe4W@JRWRuwm^XIiHcWP#)0ze&P;%%Ke1ef|aiDpUl>!Mh}>2 zT7oPwVVC=unnjePlfSnj+`D`0zRojiZ6r|fgLxcvzLKQ}ZqdN98>z-WMgtMX<+U9ru$ zu?7<#cXKulpU}6T&ig?vtnlPqh1?*>1v>szv zp>n2&vxn?m3=lXN)nWX@X=8XUg)MWJRoeH7>o4om@N+f7x{uH;oXE$lw%Z>`nqFF2 zfeNiu%zFo&USngt?qMMH{=&dSs_T?#MF7Ym&oE(gVeSz-+>00mW{H%|6QVbi`4-r^ zB1!;SpCV7o8qUWA0_gm`JTf2gPv%RXceRVIwgQ4fTnvU@HLZnqjWQ4h5PA_c<|aM4 zG)VO&|C2(hsFGTZLaLh!;e4zA{j;?cH&r&--1l|Mwapla+el|S!3R!`05@60tPCI82a!In_1B&U>`a?>61PwR7 znD7^~*+qC?U1@N(B*5l35P7r(6JjQrNwx*o22@zl-RmpelzG|q2W&k-X=HS59Dy`XsYnb_5*&M(S^Pl~0q&PU> zr19&URP2+^e~Rb}EgBo?S7Cp~1!Sh+q@+}yMwZgZFcmXjxH2Vdd-4RQeysvW(Cc-y zze1iBauqDnCmLK0{Kh16jh`6XligTzYI4R>$Zj?8!@Ci`%BNUEH)21HKr$HbNv6BO z873rOeVDC)|2^;AVx5|r5AhumV`D0iTTxJuGW6hW$aPq`dj*^}00Gac48$S62qBTv zj;;wsI;gC1L%M)3{cBgu-_06|rp5n=ue;)!fEK+V>Jp$~V9saplrtoIa>9#?W2lwd)(bTPj)miTE&z=Iv!OxZwvQNTZ(-?{PRFjrU-hy zY}567Afn>3Jwrh+ay;Y!`*aFi{FaX-VJGHC_<{@J?;YEudyG4t1`r5fey1B60!*w% zA$i1XSiAi5pS*=e6HI@uQmQPE%Wu2I4eyMHOc3J!lQPIk`;J0b;cu*6ad5{>+CYV_ zdjXd0Mkr|S-t#8)ULCXjBF~!X)^ju;pEcZ(V$K@{V6HEWHQCBqU@JynT$~|Oe6Wfb5Oz!_DmQm!Dt2~ z6-du5ruUnRauCd%Uvf?Cy7Mhkd5Z&_lKDJ{v4!i;B^{RVFRX_DO6A>H)C${Hu2FNt z!r3Xj*HjiqE|9p1J*b?lzdG>Fzf9AVm-x@ZKAMtOr7dEo6R|b5$VS>04z*8QyE9 zOvy{uW2KbD(B}M^V~0F65)=7j!5QRl08R$b6DH#6Li3IkFcbjqdnvNWN0TazP!`DC zXti%eQkDAx;dD-*d#+W?dKGrL65V0Rk-G0JD6V230Eyu3d6LJy+MUUuJW8s6QJ#1L zVfhzv=Y*BO*#|P?ZTTmvUEZD*gB10B_he_d-@31odgh**MJlC8Ox5F)KkV2G%-&t; zFToWSo&ntqlAcOI!Cg%8C4z-{G%vxZKA0>I8(5cT#n_|=5U}1XU5Y)y5b56xZZaw7 zQwOM3+uv0|xNq(}YGiK%2INXN0sT}DdcU#WNc_iWtD99n zKfnpd`_otTOXXi4AP4S;{ug<{H)7-bMG`m0D9r0sHxWI6iH&Vq4w%1DfcT7{5R&IO zPU*E&c=%a60(x-X!*16D_`vtsOLl<1J5<+_y*#iVzb&!DE%PydteCFWhxO+%FxS2x z@-@BqP(&46;dDz69nkX;RZ>0FQni?h@vhvyJ)o=EBuE#EJ^puOld=D$Ovn3ILHCJH zz_Eafq*I>3G#@xm5s`vk3wf%VdoSLHV>KYpZa@3&f~IibOLb9be%wi2Oe_Eks<~||g88`6Q3!Ls)k=DyE>f)UQguVo5(q~w3hZP5?7KlPaNyp5Vtk>pi z>JJ3SKtO3b_n*rFhWMw-N}7S8j1v>u?!-i3ER7OH$TTc)0#_~j?}=N+v#nLdxu(ZmN?8jv-b z`iN|EJxN_HH3QK<`N#ff;Gl<6Oro@d`s=Bb@tBR2p1AbL<_wJB>g%Q?GP6DJ{ko@Z zSrJM9u@D}5!12KcKqw#>zXy^{U$g0+(_RVPq5voX{N=2T3k%ko<2G<2RcrP>fCR;> zM^bk*n+cLS0~v%OHUqzVlj@PS~k!{U1MMKzu7qiE%JqGs)gjsGb9j zXA3RVz_XG^1G=dfhCb}#^TE)K5()>j;Y}lP4p<+sh>1Fbn-2Msl~HW} za=|QZ*nl;g0M!?TiWmERn=Yw1N1|f5q^kcNzJwY?)A8E+YIKx?cyUv7TE|=-FR9aS za2E`wb7#5)lK=yyjdw zUp!jA1|Y_3^fv>UuY}^Z|FoiIM9Da9Mt&6fTic>K4ZvAOn2e+Pc_62@eiXP2Tjq3& zas|;BKKhycyqWv7IA$)|rMhZcNsJXS`mLsJZFc6)&UOTMjR}^Z@TbDSRaJaNPm zeO^kYHm+_2p$@gIGXl?^;}~sKZ+L(#-bIC>TJR1qmh1NCF);`v0ZjOu)hYp(>{C;c zKOY4Hj<^Vx=n;x3Vz~O8Q!Fn`VFRxGrd=7_OSiyC;&C(OdIvEbG%5$1`8q;bI9MkS zXg82(2h3&wTY0tsN4w?3xhy~QToHHGSg)!mHL(L!oqc;*kydP48bA~grQzLi>5!`Z zxt~T124nxS0O>ISmaLfgZ$bf!i4Pwa*AM~09?uj7p$!8c;0Px}Pw*Hz1z21Z5!-+Q zc-^S&H+fX1UqG7w^2d|$aYJV|_0p4>%kdv+QDlq{D9z+#_`Q zT}_uDO^0Lt2(P@fvY@p2Uuk!Cwa97F@M-%rJ!M`oJqS@D(ZA&c1vop1{Am~a!0Xm` z@%_C>+ncf<{Av--H{Uj8qTyHMt{8;f1+*?~~@M&XbDg zh+b|D59qN8BV5d)2OJ-g%K0k@6L+DL1i`Q<{Xl^;p`=`0n7af~U(C+dRXFoCc*O;G zHQ)X4+F|Bx;(AjQgaW^x74()5KD+iIlopk6Wnzt@uI8MyPNO(zdkH@hxJ1p_o{)Tp zV9GAC*7AU%RV}gUaXG5rh(XK6rL$a3IPG%K6)xaO`n{Zfy;-$a^u;o-&%L3r1??&X z{g^_iraJ342*F|G-oaxwvM^hkCM_p2^RVm2O1~b)ZS%CTkGv$BKj@euG9S{9Jy01L z=1Y3%Aw5uscJ^{8xnhG#Ic%F&xc`YfOt!%XT;iMYvaMto%}}$^#zJuj1tIpg7d{iV+_i4hXXkmH3+tLe>x#|E`-F}3(%qFH= zPd<-_T;+HBc4RVG7jJgCOTLK~UmZGZmfG~m$w^2Wy)*HLdGfhLB|s(kBCzZeOaKx5 z#x;`G(^7vv>NsSM5wsEyC%2+j33tnQ{RWTIT3q!G&CU4+Yr?l)K?D>Pecsm-3B1|c zaKcYK<_M}sN25ebkq<&c)iC1d^m*_7G3#ldKRurW9aY9NAwypT$zgqpwbqtqE5l@7 zyfPYC-<5W!iRZ1?xqW%{*?o3&pwb>n%^?vfR+brA;F|&`jPN$@1I5JSqi75ky#OZY zJv)1hQqiPbxa7LUM;ap|qmR6tXtiS5W2#e@)ilRV04=yb?XB1&)f&0}9^>zRUib3m ztm#Qjk>^+aqaQq|is{HRsvV?=G}XPCxnCkEm}{=8E=^}|9dsgwk9Pdaaqu@XyPJ@ESU_rlf0q(l#QVZ>ZGa~B!| zDVp_rzL*nxy?_OQY9oky&wOA{WudnCO(se{0^b@gUs89^_&zd@2Dw+K(-`l5?WncY z{iV}zQC13{lRwq;<|u{tL9x%6b})akg*d2K%MbS4rz655y}f(9^Po`-L`z<#t-*}q zA0TiioDdv`!ArAyYdrjF2aJl)>wqEglhbPIJCD=W_s%z#US3`yrKL>uE{91dcUuTJ zpoPUnX?c11pWiI(?U^fmpL}K~Cu27@H+h<_g8~Ct#>U3F`%6^lv5ThlJi|3>tP{mH zp$D_ki!I)bs$W^!LNLh_V(Bzqt3kvLt;Tavoi28i5I66&@BAh1+xe z^94tcVaWfZ>8yjQ`o1=P=!Q!peF3FQx;v$%r5mKXxpbFwBOxd$NJ)1~hk&GXgLJ*; z`_BCSV1@x^xc8pD*Iw&+K5K23(OH&49ZS7NIA{qjv;o+j9c%A0cx?O+nu+Ss=L^rkC?pM+1_{WY#B~&h+w4cSeqfJ_`Vb;zg zNOH(GO>s_*pg-1~-=fg>B zCZCg0@B=O`?&Q8Z3AYWTKJdx6rPj&z&rcq=qeae9L4&2!GKPGju2+(LNU)B8hi{Kh zPYACfe2~ROB;}=mKtNZ~mI?Y7z4RX`>>W@YU6hFoT;ETkt%t&kcLqKU4aAd;A{yw< zAE2v0N0*Ac1}RN~?i6~{wHr6eixw~(_@P1|5rv+E*k{<7h-stg%BXPvvR(nOpf5qe zf)pVx=BGwKuyJ~ix9DqaAEmh|L@|GRooq&xhmZWRaFixNCgytzmWVC-e*A=EIyNqm zAW$HumUyyvu|GH@uOwZ%)Bq)m|L?owb(5p)bHW4n0~nd#4Rk%67&2Qg6el2HNgAQ^ zmSoc5zD$>o7gO^SB@80zd@~k=bWsI~=pL7fJfGpZ!mTqWvqKR}#QKcOpXaTibBcfmL4tMbNTod2mngphTxO1-Z!%kBR`e{-tcE{Up^>65Ep1ho^ix^+{`K@f!J!R#U|tnH!>ncjFIu z|i37aX02;!9ELbLqmNYRk``2Qum<3{v>< zlap%R-rgq9k9X8E;o{e!+b?>Dz83 z_LGDZ^Icv6-0jNaWpm3(9W}DJGH?aALX7y?&fA;6sHiB$&KMPD3|FFi70XkDe<^$g ztYdX$XxwKo#Pnh?UdjO*R-*4pQ?2kNF?Z^8ffIlUp3k z8s<6-@Vf{kAVti7Z|PHZ=F#_%B%hg1{04Fj!k%04gW6xjR)l>ky8@a<(m1_s$u-!F zBKxaCS<>Q*8|}G^u*xKE*vri4k~ZX5n!xm-XHV<}#_|gH%3luC zh&=X~N+nd3wiqa^lYLtmi&4%Z#Y=i=tvNS<2$7z8ug zI02?Y!8tlg3?3PlZf0_ff-2F{XQ}e?z9NmvPmq@?eFxWPoknbLlD568y~`N`Zc+|l ziTAj0Vub12$SM76!=M+J3v0bQn72M}{$>Zi%_Ozw)BTkOG*mMWqA`alN&b1$p2UP? zV7$!-nedu?w2ln;M;!(s8(^?2{_5bEf&$aWe*5Kvi|QpqsZkh22bdcV>W5Ov@wqB~ zl}0s%6MjDy=y^69daH}}r-9tN0|}Ob&D*=i0!t4!;^}=w-x`+G@S2~&nP;N=0LaFZ zG@=XZ8XspR>w2BAB}y#gqI`8QH=HNrmHWIwbk=?8j|qb?o1{uHf)j=Y8B6tc-dn`$ zWi}=eL&Gp|a0-YB2|ZfFl%2USI!^#Qf{dH2PnK85W>+uP=QewIFq-bjEIa)wp$3Xv zKbfmMq-Gy@?=PsqO^UCs;ACErm)JJw+i;UQMVQWK56%mwTl2!wQl}As6FkWk#?o;# zXfc4Oe+dV*h>VOJRLaFSA|)Jlf3+$F8Leg;P!-YjE062 z{=2je6HJw|J9*J5-l$4$BVSD&E@$apXsp4D7cNw0O_qJx9IC;XGG;beh-06+s#i^R zKQnobZkl7_2B61FJTSlu1Z(?}5zeaL(|0EP3qx%7^%DaqN{-@CXDKPmmrBqA z(d}SM-4n$Ho={(Z9t7J3d}rrXRsPP68_6|mLxWnL3lX}brFgzy1>FV7?~^jpks~Z! zOt*I6Kf;$rz5LX0@J~&?UA&#yZy&xCMw9?1E`w92tHY8u0A!+z7}Rw>z4S!eh9$D_ zG!d6g>9GPas*Q~e9+C*#ZjLc&K*~Oty40Nj7WB{e>1lCMx455fSUn~x>dZoqm!u07 z_-qwiO-+-rjl1*Se-rh#_J{dOvrDCZRkaf8IenNCBFE~^6rJM!A#&)(*j$Cq=_z=k zGGPPX1>4_qVd5iFXF}c*i97j4VgMaQsgAT^xrB%t+Q&-`3SfHn8R*kUlr08V=)NTC zzU@iPH_rn-X~rkXngtO39oQk{VKX00=r59qh2Y|D3~(rMNy?MJn(Qe@kvVX>RP@qu z!1v|HV>e6PF)hbi@Kx7Iyy>V_O_Yin_r7}QvhHPe$h~5w`o{P#jExcuv$0&ECpO6G>4$*YrcCy25xmf zXCkC(Q25lo*ZqN~1;>pvxh6}!!hk9nB)5PNFZ({tG!uN8h-aSANG>i5y=Se8xi_6p z*UPv2Wd%pomZb3mu@F3Y)M&oT@^DvxCCT#6N#PUrf3Lsp#}?r1bg?n0WWQW}ruF^xi0)?<7+SgPrB>|m@!##vyIGql_?G?ssFTT8b=~Wa zDad-Vz0Wm;1qFnf4nPZ8hor0)O9`M@j zg5+o~&ls>m&$0x0QE!fyR{aDY3br&R=+y<`%F`)}(et+4tS>llq$?>eyoCp0A+0a!}| zd@5kNT2gZ8n3%&y6|LO-o9?4#w`iGIR1tqE-SPF=nYgDX?>7Glj-)(Q#oJC|nJHVl zQt-?hVHq_-C>-)Y4(%4><-PvWKi?i^;(!B7&|aG~Cg@lGwJ7g!AOv3+51({UG-v1a z;pnG2{O-Ha)=(52Sug;f;!83y&(5e+%u%u_Ecf>G6lzz9n7*a>u9i)w5Ga_Z;$$A7 z!BUWWzBOpR(Gz}sPyY^r$o@LX_Heh`#lT#0$TSw99(7I9u4vcJz8m7eWN9m%vbpEN ztd}sQRK}G#M|u63i<^6PydNZyUQz#^ZXX*PgO$F^*b&5lWj>gKIp~Lq>j1WxO?SVw za_tIE&!a`^bW8Tl;6{r&YE-w}?~UPMVcspRt!4UY4$9+568z>_v9BLj!Og^#9-gPM z^nsw?@cP!`Y`vQ;Ffh;ueT!d-pnpzgwjmK>2KV!O2z6Mx#W-iqfT22k*(W-rx0osG zL!Xu9h`1lclYF8~LSP`3OGTkB~LI17gH3yyJ30LThE|^Js9buY#@*=avbT@Q*ncQR; zhpK-Shbv!MSwA?&x?$2v%1?sv<|5~bazv+O@j01FY+2m_<1GcU>h&HU-S2Ifv~gY~ z7r3-5hThvuc8Xz-`SA}$RV3n&UY|zde~&$~vUU=+L(~sX=$+2ECjzKbr?ZlW(Wpcm zSN%09&&X0Aat{=LPFYx6e?a#^(B^%REuQ*X#v_==hSbG;eNr)Z@TZPt=Ct#rW6|;h zBBi2j<&{VARC8IJ_?Bn*M}ZxzL3mNcB~<|CaX8=oYo7`gdi+kmV}yH{bcvGxz+F&e z&?O=JGsARGI0oz1S!9pHu+hs0j5N}C6UHz^6S=l+u{by`$$qL$?$znFBZxtU+{o90)%${$pz)7Ri)8U`o{Z=DAuQnr_G z7DI6K(otfH`15vsBJUS4SsF*{K(}Xk4zo9RlfT-^<;R7Ly1V-i%T;5Pq>?jvS#>`- zX=y}hb)}M2QKyEJyTXus-TRRV9L+CfX(xI5b(qvJRdL{L%BSx?yo?`-JJHK@s>AF# zB)3l@mdjM2U_5|-MtKz--5hYh{Vz-Jw$xGT@!^Wkxk?jlpv_=N);#$c{k4!8^_{}) zY(C|Xyu5hB7iGw7u~73?WH*Hf49!|)V?5TH^SmdBr2a|uGK9`NK%OD}hAis%GzbWg zebe^%q`viA+xz)X6Q|>i3Z`BQ=Hq#9ruPZ=&+%JKy(Xvb%#6tPwSdIf9A&d$C|A8b zEKNbX>*;b=z4hr56z8mN=PM$YD1=N*3|Ovz`7Va*jm#7e>KF^38Gn~zGfH!|x>J;K z;|E7c37_rK!hx&s&=ZhPjxCB`FSDQx!g*t@2nH1L&=}OZ2O{%5orrP3PwwA0!6YEb%`N1PLI=7iXw(*1;+Mo9NMpCS9ZpI!{$l@ zLZ~aVg?--UHYW=eRjC>1HR!YU^z?|MwwN&vlD{7O{kPVw9x2s@JhH8=t(!x@bU8tx z?Ck6e8JWI(3`qhkSQ=GEttosKMn^H|RdRJvQlz*{*5YbP+>T;;w#iCa7wlzc#vk3{ z!SISlN~)jfgT*-h=1q(ldZ~NWowX4f+^eM~CmeNxCItTCGkINS=i1(Q&5F50lk4Qd z)M_yNmME4oB`Rntn4&NUPkb~rmqlCuP^H}pWnmdJO|s)v4%MzuE=lfzzP55LOcvIOHiA^)tK*C5#A>+)1! z2x^zz$M4H(gMJBP8WE&SFs5p72L%x1W09dSiA9a~l+W>hiSJ1^Chp11K$-7HLNG5r zyPk{9^cO-4-7*`YvyPD2yLMW@4$T-aRWrc6e|)6<#%%**iYNW7LGj*9x*;-h*yTey z96F>&{CFn;LF^?&^fFMqX}@6i-KJI!x2(wm+gYVLLS+RrDHb`b&`4xst%IWH=2T*Q zzDhaP!B;=)7N*-VoRk;8Sp6qC=TnSJs~0u@g(1uFrDE{pK*v?cA^EEP5`RXbs3UiH#&c+{1*<@B;ks9{(k*4SL7_dydvIv z74pIHdVAsiKLqbJG#nXWh-FstRHFXgrd!lP6`kOrW_ouD315aIjo<0lAP^fqc;N!p zBdG$N-xTZ+bUTGRuK#zC*fkKmDZmA6w2~Vu_C%ioU*V=@mt6mn%{poQAeyZwXdM4R@otiEb zj-CDX)x^J}V8fZtz9S|cXOmS`lJn)I$YNCi8C|}H?zus-xoqD4Jgyfgm%uB&MDl>M zDjy%+hb5A}PC@czaDJ2)DUbBRs{*sV143{54fJnKHrOw_%TN*M22?FgteXUx{e>H+ zccJ>f5^Y1P;)^ZqsB;H$SS`!_N8+BOJOoHc99gt3j|HNY&z;$Jmkx&XWl>$+Pxg)| z)>BzCy)`)J3JoMDBf&_@HGizNpAtKLNd$JPsvLG664w4R&*z;JZ}CRLwX=OJ9&6f? z1((pm1vVA5L(Dz(j_-KkwSqnIC093m0<9EsS#)ZQ@;<`BrB2V*-2(NmqRbrA>eQH5 zL4}S027iY}5)sX!;6pwb-;029R1rdL&lc+Fn!dYYpV_lUd9>0}Uw`{c@^$)A)RS`@ zYhGg^p4msixh~gFJCn$?qs(x*DVBEybw;5KrN7`;Z2@giGE_R1K4xY_gz5G?uCgg3 zVfLz3Y5Ng$*@e516N3?DF8FOFzCqj7&05fV6z}HBujRe)W&t)P&#NuyPmfptSlV^= zVQuhZgQdyQSeiQd;1$EbikGemvjd8iwe?^(LE(l{`0evw{Ewug@ag2X3)SWikuw+L z?j-5`*`~#l`^7lxJBP7n2!2!hL8qaha)0xxVEHWv>Zl~Rm5~FCW$g#6`Cm1WFnRIO zoh&DgM| z2A3Q!i}|4ezCj344AEZm#L@ZcTz!mBk_36M*xcE3j&w!T1wLm`wXbbyLVRTWKM{SP zT)DGbhy${vN*U_q&~Fmy17hDm zBwHLzjp~IcQBE7P$=>R9>e<7EJVCI*qP-Mu0_aU8fpqX-#*ix zP3&?_UM|;A6g!4H5K+#3gW9h-8Z3XuXUt${ZV&t$nGF}E)S=FH zTi1pF+3bpttU?@oI}9scIILPurjOb5RkV%D8-BzdbfMaJ|5@ZOY2P_+6DT&QP-c=C za3rlB7tA=kCiQn`r?GEJa;))phj)@Sl$o9Vw2&72b7BmbICQxfe8}J3RZ|>gat!>c zbn#Xh+r1Ba;*HSvJRQOj%A%hIXZ{1gp7m{_wO>l{Pc0;m_2-n>_P7 z6*TDU{DpMUDZ2*b%g)jr<~)@fs!~lgn_X|P(m(yV+FUZdxx*4%2WKrn$=G&4pg42dM@p5K5;tLH!UAJjBiPQ^%W*o{IK zzvP)M>M-ggei%#$Hq`^A%zv+5?fu)(^q%_`p8N32!@5+Kajo**^x?)rn?QB~NrW=~ zo1(jqv@W)z+P6(kktfDVd~?dE*8yT=&l62@X|h=ARSNi;ALj|oU+%~{JVI*&?HTmL z;sz#fw{5s>s3mWXB&}TEs<0Yrt6?imI46M59kG28!%#F2SxTG8?uRWFpD`7RQ8CBP zPFC*T$2rLqXid>Vr4z@_X(tB{Em95Jhq#d|WyXd^m3;X5{!(gK@x|>hAN~lIYPvN5 zYF99$nn*%`F$HTv-?2V3KDwxRkv#a_{#^}!!VJMCzDA~x%*p%OdA*>5>61CoHEb3V zHNJFMm0%2@<>j4Mn=Y5JlmJhD>M$h)j%dr@vfD1=n_F8Fj8_daosoIEbG$-_W4H-h zlCAFhJwRdU6uj`hCJCxr1ht-l-AXejCkZAMBgHxg=H*nI4-PYYAUGMvnVcBA-iHF_ zge1`%e^HU9wL%;xOACrR6OsZ=zl;FgItzub%zSc+@4{Rm5ag~fJaR$)JULK`@49>> zGcq#z1ZD>Yt2h8txpYaS5#P-JSOAi0x2H@Oo0nf7Y=mv7r>hMIv(<}n!ERl=8!Fhy zIAK_l|4S_VVOA@dXW)eZ4fd%VsEBXHyID_~D$-$3ybyxd3fxIHed?5PQQ#$oldRoc zK6So1(kn4)O-MLZmTj<7{=Qx1rC#swv03YD$jqpY0+*M;>0V7fKt4 z40Jv3O@?Cw&Xf`zQS<*{*E#0kT^w+|6_1xK7ZLe>nKA)NMx6F9>$DbhZzK!yPYAH0 z-j#xw$F}>A1XQ`dRb=ul-e}g^eda?krJiuHAQMpjqT%!ppV;K$kA*b(QlzNZxXdVL zO>e6T#fnf6rS(E(udJgm5w~sdil)rXKzmAxQ*JJp{B5CY2~V`5hn&5`Jzr&Y;9UyIqbK(UfT_c)HPCEa%r^7MR{j>qR#_Vr zh01}sWhr2Yl*ABYX5e#gQt?PTksW@woY?YW3l;yFH|?SD$y1>C0{lyo_<`3njF1EPs?txz?=fdoOckz0ytpBI{ z{CsjMs>Iq_4)7l;pnf0w6B|P9e-<^rS;ueSJBZETm3EH~*`0l5@aLIN`0=r}GLi96+~va1 zVzn)LZyj{%D(I)HjRu1%7P^ws(h7=;G>(_be>d{{s#yaxVtVOg(X@N2|M2sl(!)4x z-daF>11@=pJPxBloo{0-hVVb?fKN-KrNvY#^9xL^#??)#r#cAt&u1=F=$T(5;YqG= zco*fLDeraQ=-wYWK}IH&gC&GsRnqaBo4@$KtgNULo40%A)6KX)gE$eua|JQYvRSJ- zZY7mqSshN>W4_;%@HlJHmFhnGPdR=Jul#O}nFGLt4pHnJXK8sT0%sjck9iL}qb%OH|<{JKhdhfU`^V1q&bg-|_0=Shh-FgY$_0fr?XvfP^ zUB6$KkS^JK`y640v7)Z74k|?yaI*-2Ub@>zH;IUJz{5^i!mZNA-7xserd;*!U`DN} zsc8ij)d18YEq`MOPn?40hm!*jN>s|r2(!9gx`ORMedVnzB#aMkrK!EU&fWk_!M*e4 z+}*iJBC_D?=V2P(eEN=ATsoIqL}!0_l$}iLR}rO72OY0e*G$#DpG+GwW~@#Jd?ug`GES_0dI=HpAehVHlN;Y%+WcLsR$EH?@@}6_E$zzY- z1Kl^^g_UXUYu?&*NoXVlG-Zum%xT3@x#e(7{~3Qc8~B}a_U}^4Q9s5^1q+2feb{|$ zr|Wq2w_6mIFu^4mPcH~IjH3}c7+I3gQSzgHmicI~*o@Zl)pqgX#3EOhK!^M+nV(U> zRLW;-_yS%$pJCnf^U;yLoysZ!LRWt8&II`&*6$y-DOFg{FOkK@V1z|b_!Ow#T=pgP zx`DWEb~6>+4U2GlGt<+eO?Y3ZqR+6qz+nAv*XJ22r_+&T6#K8wGi`+Fg~&Lo-smU- zDjGI#DfeQV4;Oy|KCmeGQiF$RnvjtJPC(DsTW-P@SHC9fW*Maflm0v~m}0@SE3}Wl z=Ao#Hld+nP2Wd7hr0m{NOF+P*^+1ojjIng8h~b1KqSm-tTpp~!*pGDKhU)yAFb~8` zc7Mh|Hr3(J{?4|qw#jnreCGj09l`7^DpW3ju`oor#s8J$=zQsiJrZQ2h9^#oehrI1 zEulw3Ugy(o0wkoJm`#aPa-e6~^|KkK_}XnDE&fg8thI&7@+?lW=eFH*`+vc1`~K=8 zfQq4%!@{yVDFwk?1slHAqkDOw#$?TF%@o8Mzk`O6_uX7A^4g@e<;F$2*ms8m6y3#2 zLg8;(p-VG==d*NT7<7$zKAOz#tU-VCpW{n*{VX>f8tFO4rzgyNWPQrc$&T@;iCnK? zJT?Dk98@@JJtIy6ugFN(-oY`BR~m-Nl=xWgE4hQkSd)|8|Jwi(W-C152j{J`p=rxA z=<)>0YPWRoRnuzv`lM6)?im6JLgJ`Ct6z-2AgIM*<5N-LBy1sXO1xQR2Qwq6NP<8Q zM_eyG3TCfx0xvQXDpQG8h4ujvtNyHso+l%4=-uV3xWBN2xlQ+eW45$}E~dy*)P_G{ z6dKm1)yEu467j0f#X!l|_nwSdyK;y&Z2cwKR=b5pouJH({N@d`%sLU&s1`0mM|bQ8Y`@g;u|!($teBdWgsBNlNkd-Pdw~+O4{1s^@D$WTyS~oh}L#E zD?3v9?hLye0aQ?8{n`;DEG2Y_uu%RD_AB=%mavn>f2mFS6!$Hj%yc-Gy|=#=4^vN% zW9M&X4}5DYLRc3c18VWhQV4irs1L7EIi@MIC4JSY>8-a?{+IW{RsGn2#dm!?IuVMu z&`xX+;J`i4RJHv~6uO3juk+JtN46@Asw~9aE zkV#=$Ofgv-6`E4Dx*!tbg=O znwZ#guT;OYG<*9Lx&EOhsjFYdCxLZU=ix3kaQ(M{ zK>F?*FL-Ga<)5US^n^~aXT&UpVX*i{tm}pmi-L1xuZxDF?RZ;gSV9-s{;1b0e~eqH z#b!pvl${O^MLl|i*QhZb%$u=BACvfPBTdEzf1>rxWP-0AU7E^#)~=HDExdk4an}~M3_hx)BEhdKb~r55AidE9S6O$d+a_DQ|9aHbWvfHX zmU{cMi-le~Xk?B8&+ixaMBmC84|ft#Qer1;Wh3lab^HJsslS8&M11wq$wuo8#&>Fn zN`F9MqN&sVZdeMRDCNBA31Pf|FM>CkI6J@!c>jhaJ{K|Afr2i76l7Z=mg90g`;kE~x_TFN$)*M^urB~|JTM-g`io%m*naXyoWoiA) z@lr)^nnx2h6b?gLd&|!-#4N4*x&N<|zEa>YJZK$zX$RE+cNhN*X0&ivGShpLPC%9HfK^>P%RMG- z+`3H@Jl>svxu|GTf$a(uFuSHo{^$F{&kcT+|AoqTJUbe>rY&xXLax1as)oh!QYDXcqx;Q}! zp~8m@E@zh+t0c*yH%3d3DCI1))qd>TQkXGU??2%=j$h+2WC-h0D%ZvcZusnRCz+1m zMBvHbZVe_5g5FHzrEn*f_(Vt@N`2by-E{~xx$n{f-zdN08$VS^#wSIn*trIdL3sSOT_9*ubThZyZGIFx22_J90_8FET{N) zoMVhAVJk#RQW8Q;e8WAC1`mLMpy@a5s&8}<;cEmUg-_0>t0H>o+d8izep}1&7qFp* ze44+Ho~Fi=)WB%bA^XCzuJZ6k@oOxFa-=jP>(}9oV$$TR5Nai+Be?wCAOK?f{EUwF z#8@`;Hw+1+N9Y-j2|eMW6p?O5(*D0zd03NkKEIGQ@)blLkTt(!e!46+(#toz9szC< z5n&oJAW#RQBarMiK@z@13b_*P<8?>1AL-CWqnI2NdpHG>vZA$HS9 z2T1&Xfxl(cN1%C!_n!-SJJ*f6w}KYbcI6oORL$d- zN`bCzEXq=1sD9OTpwA=!yNlZL^73UsC5K(?!iG0>+DE_2vr4<-4~W18K@d0e0D1z- zFMbN zKrJQ#Ts!96`S6PG-o0Z%gdqyHt}&Ks9k09v9c051PG+rNkai;+3CJ6SS^!F{Ir(1M z%?=45s1drTY%p&sSqSw|2X!fN5@c(g8kwryrLJPTH2Bl6fBt|}`t?6R6H;J2@ z%f?N-w}&mcV22pe=mSB+Oi!sboW=@;~F^r?0hii)MTzU`EDs0 zJj0XS#0Z7Na)L#;thV|Z#AdtCuIohK|~XKWzoyu#UR%Ivo3V@}X%mZRa+H5R4A;HB>3qaOaP+HFUp zFv@oiA^X5f2N=A0<4g2C`<1p4KR5|9u=n&j8~-g2Cnwj%$sgCz&_qS&h2^iybLty& zCw(S*hprbRg~u|sgBUCaJAbw4cz7DfU>`kKg-XDLS3fKJF(|rK^n+=AP}&y169)Jx zxaA<%2Q<8s z_%C6taci~j7qXiH1kqYLkLjF5*V6yx&*Kv%Ac?XQM;1`|AI|k>Y3LCNdKRvV%GP*pWZk&x#=i0nAz&p8r{%`A%; z<7d`ab!ii*(M>xh96Si1v84G-K zKBQZMXp@@j8!se|>0E);Jjlh@yL(R%ktM*zRj=-3=H?~<9W2HDNf2%tbkLV$ECq?? z=v0l@Gdxa`LzSH71E~tz^BuzBG;buMz@1I6pb@}ev5q`x3S!o5*FD@~a3!fqY4C+0 zc^ybYqo$_l#eCeWI+Lv_?;Kk#L&2Q}JAykANNlBISu#-ECj}N5OI!YX(yCy-ZKC1Y zqoa0q_x2JYqQ)>BgDfzTlbu-e6NqHW0OG~LPjHz^>6?+56U0BhvJj7Dt1zncv4x0= zAeehShJOq??YlB;y-==>YiarKqwJ87a6UhmU0gM^S?n~1lgNRDkiDD?zZ`27BY)$z zM?HlJE7Z;0&^Xa=wx9_U@j8XW zezC6~A7*q@2w`@nv)0}$pAtNb81*qtWb<(PUnznP7$(U1hgn}8kkmUc6W#ctDI??f zJI6?%Vn08|e$AD{&S6RT8wLTX|G4sNie%4Y!R&TkJgKdWVyopwl4b_d%6Rji4QMujFUQWB;A&H|i!5wPU5_?eb7uY{(p?^YTthCk(k4~vvThN12? zh$Bf#OG{%Aj7_Ool^G_+(kV!S`Ltl^Kj!f7k6*n`SHny*^R)vWDF$8cIuN(hKteAm zJKOg?K|c=}pW_%YTa^kSRf|DC2PUqJ%NrP7ct>E$R&(I5zn{;}oOSYd$>2=L;h9nrWa_8QHq9!%fhU#)#FVmc)yP2s%VB;OB_^VQ@ZekfaZSiY;Vt-dm$eThD_^i zXn$ddsTh%x3kwSqC+IH?p%yNlvttK=B2D={RRLv16^>Dx0TZFOMCn(@iA&wxCbM>+ zEdZ9RaDb>sC;+_pKO=z$Y5t4Kc%#FV*}ro57EzKkbYfn|9&poK_O8OidY7c z_=S;<|0dUgvZ&lh|2rf^5Ae9Gb)f~QMWAqY?5h9rFGXY>o+-f70M1m`Vyy$AQ=8Sr zs?=8h>&l9=>5LU-T>QX1ds$?oriFj&pr&wj3LqPyT00jQLZ^i1&TJo8J$6JotcpqTScGV;)RU; zwdiv{2=UwfwUtHrhtsuJpN@jxM&}*KMS5S1lQF8ozlh)gJ#|ME40IjPFif`XeL^HP zV%XB7pQ_%EHQ7UT1A6nHwliJUSbvCQ}r$S6VDa$o!}S6O_M0F;K4G`?GHQXEfG+v{Lf zVY^R&4d6$iF3I$PMp2{2!WpPHFPtfCpVR-aF9Ry+!)a(}s1-Dd+C!OjbQVr?r(VF^ z&qOaBu=(cvz*h+SYO42Ur{RO6fdM&7v=QivKm0&~l~W6GmuXWdb!4Ri>p?N_p8A5% zPdgvl&es^-bMGnj@MK2y27)l ziqxLWoY-}*wcw>c6b|%fi+8`>!L8ksnNobKc$LlyS!}@guf;24_r+eZ&SqjQQ$y2M zjcPbD(d4qv>nMC>d~Aj+a-6XY>P1BcaI){bOJjSuyKC3{g!Ox71|LlQE0j=QS$F&^ zV2EiNEkB1aM-r}z|MWb;bkG2L#-Is5)bf8^N?0wpV}W-VS@Dg$8=P(s1@giSFpEm~tD#UGh)sw(09 zw-ppjK`5m~trKgs$$0rYg4AY)>0soHvqiV$q@=6E_DU;`TFgeRg>E;!`j-k~$Hc2W`deZ8hHBY;69cwrnvbJw3hIxjB!R zIkV>gy&pNdK5B|pp)w2Z&h=^rdky->3VP{8-+V;21te*|YfF|4F{!!+*Yk*Fk5 z|D)WnkyxWzaDM1l@g8&D1~SN4x?y2pxG}RW=5SeKYckDwWAK}h?7&i%_Npffv@qb> zojp8W`?fz@3MDG7^Ph|_HGIsF{%HCqqFwvt1%%rPh?}+Q@AAy9aa@Reh~T~RMpk7+ zs1-itP;}f#3eYHiZLJ4hKN)^EICvLwyLqdt2;@c6d?ueh@Cd! z#-+p;Y4uz~*+3#kY@>=o^bxQ+KFoBz^i7a10T`s&UWaw{&)|cYO+UW3c9m>jcRQ$F zAhd!PI7RNVcl}Zo?Y-*sB5Lpfdn;;cFYEW4e>w=+i7qq_HRea@g^m5qNqUV|y(+3Z zSuOyYhS4&Ht>zCeqKFmRQoJN*F}1*duvy=I!+IiBu<9gN^Th)1e_(>i#EHq=8|7%I z5Ltc2t?78wLQ+O@F!P6(9*~*^08l0!|BMz2Z@RDbh-w%B*Co9gfoF$a!qZ>&&E2ckYL29n^xx!Y99F2EGSXh?{ zhe3p^!Cj;9#EZ#r?$J=kP#p_5sWJ{gl~eQTWUaF^GgX58U3Y5#jDFeNY>#~cPzq0v z_5O=^#oP(NF^i2~|C(_@;#;Y+G7`*%gXz=z`-M#y*dL{!AUbn+1zvdQuQqL1$CUQ% zdvO2+l+yB4P!hI&!r!jOB<7aFoonw+ScSJIWn{>io0&OkDEj^0Xesc?4|-QImrfE! zf*Dcoy*@@JjjFdk)E$zhzdXMoObURcLN1bTC_(ic+s9W%`$dYS7Y#E5cE8{SR1WIz zWzLL!1eDpef~3LA^muuBBQ?H}{DWEkelj&RO$!L1P)(r3!wUg>G{Gamojy$Z_ezH{ zV*oacHjwD`U8ABRAjVPaANjYka8On3cA-19ua8A>|6RY2>d0vCP<&^1yPXz!ekXtu z^!S>}V9^fyNWh4TiEusKmqQE;adaUa75Z z-8yddnjY*uj`C%{k`>P&uHVnaPnYwVrFh`sqpFN62cmY3o;z^`90pA6G3aldO31f$qMNd zD70&gs+v$6RWfK8D4ee9zk%wgwx%n@j~mQ6z#uIJ3F85J2o5e-XwTpXFj>)nlho-i zvBd1MuW|9=W$Sz1v2{W0y(>OIPgSp18aM zQWYQG8<-|Trh9%}L4bJ_&P186Cir-ErWMcFs3;pO`dislK0KLomePNE1hZkype$S8 z8-eu^<2}i?S(~E^(uKAw->KxpeTa0)`c$X z8_wjywVpeqjtmY=(5)`+|5z*zc7k!?llAcEt~ybl6bRv*3N~yk)V&5{7Ft@4A<|9^ zC>ll==zI-PP)*vOqZLu$a;gr~Q1r3Lvky`r#K0Fl&Lb6dg4SXJg^n@L7ZN_l+>yDt2Eo!F*f4p?p^xE8sZ#*9x}p=< zj;g7us`@D{O-r9zv*gvQiOT{Y)70bCLjsbX_S*QpA|n(LBq0#azNBi?hGC+d=|N9( zBN;6~UGxNwL*KTW1nfp$dap=}3QcoZrC^HL%-;Nw0|BkUIUDXYRHz3k^bg!v_!DNo zKG3PA2(})j;t>+22?aiSvluj=eeO?!z=JCz-W0S=DMH|Gr6FNvmR8s}IFX8_`r#qj z?X0O9Mg;xvaKnDrN5k_q#zAyYi|8v#8Ab)oOIKRpgP99Q_~=jP_f#0eE`-`9)5ME0Mo0eF~D>NK}H zf!lu_?+JCZ#qWWUFf{zL!5cuK3+>&CfQbBzcjY4YmK{46VhXDGQrj)zihWkr@ zU*FK5RA)0H^@hr<%L|J`;5M-A>A8Z;h`C6Z!BQqh)NN&h$4e;Ma@b7EO-)c&)F zz9i&mdPP5YEyU!T$i~k@>w>~#?-{uFJL5T%BE$w3S;2;_>vNW|E}FBZYexc;)~PfB zWThk|Wbr@S{RpwOd0&F7l28AOz3z`|4|f7e=*7JlL}r771>)N$Mn#^%kXgU4e8|!t zzn@07EIu52A<;yrE8;eK5radE(KDVGKjCy`V^c-!zB$uf+9kw~QWrfH6=vM`Z57}_ zfcS82ME_;KAUObUv-(2S%hCDS8B2+>^dAkB4n>?=X)3vLloKRhszFNPA=WG$&tCc? zfXOEN@^|m*UXYWMld=En!&ai$fR1qhQo-;jwpTDaLZ*$I&S)idW2lfgM3=9yJ{Yl;8 zXN8uO| zFzna4vHg0N+6W{XJquW=)70t&DI-&q+MA+iI5=`(T?AIq@~C>)NS(}z`yN0nV`(cY z^lPeyn54W;eW#p9j^|3MWlJpBDqz95thd(hvO;yu-Gr@COO1-6CP8Q+WxP-so#?pQ zm#YtVz7ypfGwV8bxOaD~WbY${2g(v`vwArrZ~u#SV5cheD@>C)yT|oiV_l8*q8lB( zfy>j5coiz8hKIP4iC*x|WGUyrbj_Mf`fd+5ax>1tzPPCG-{Q7tI0VJ%HX-Npe1vge zdIp{IV2>LYk_a{(?iTdWa{!&;vf$4_sj|irV@1?w#Bt9Uh?Z5rHUqZ)e&Ea3r39N1 zf5*_4X?*=p>>~jIW#uaEL}-{loH!$-ju~3g(sLyQys@^js@D7IzW?&wYjs0C?Kjm+ zzMu6cv@j!ld2#jcv`mYhgf51oON6EJ3RWj+U*2_f93C9}Jot&H$+9AZ_F2!_;bXU! zmyQl`YqJ>HxFig5pT}Wm_42{|WF2DlCS^=Xe&|kkLO86&>m*>*XYD4pDffLEtS4)7 ztCi1_H;zY{I(0-zr-IKst?I< z^(8V<0B|w}D-oMPrZ(+T^uG^s(yeeuraMe!{_uc34f?=c)#y1y`q!Exz{KSjHMI4Z zv(Lchl=Y>hBYGb@(&!JmN!jJgk~1lVfCugMIq>Z zi^a93BK>d_L|^n3_<}bD1p<}C_akkympu$md&WB9&LFr~vEHk;&NLDWVq*Pc6##+5 z@4D@(|0C+F1ET7-x6jZW(jqO=jdY2GA}vEV($Xajf|P_ocL~zn-3SASlrWUiEzBO-l)p zt3}P(EA{QJrrl>scabvmnjIBd;$$3Bi0|ot3p%Mdfl)}T#fIYM+NuF;SQ><@Hx;j? zgaS?;a|c=xYqwwjH73+jW%@Zl|7~{?+j`L(bJvupzlx=^xFlq|6z69b{jN;*I0r#K z>-pDnV!xZ?8cNq5t@Pyu=@dJKM*$b%OTy=+6`4NwUmV=w77IB>ZLgICtCG@Ot*tQL zJ*YjHCE-Hwr0qa+ndx$$9gJE@YrhoB zJUooC)C+OEw)L;98~zzgklZ(l*cyjawMf^VS&cH}#$tIttWH<0dz+&3v;Ico`SWkW z(DLD%uLT9q%}{<5$c}bSl&;OT`Ff@M{rzSa5YYY&QNc`2CEVJ^LFIG2`6NxqL1_rX zbttdviOEDQE{#{6e6WnE2a{D+n%3g%?Ci7tIxCA5F%NdDvCM_9wI#(k!_}XZ^2Rov zN_aJnPSaupVUs+1GwvITQ&wMpdUbKMfvFHp87ya!EH{)nvm1 zzsV{up3-!$Q>Wg6hl`Evf?GnAI66S)BS``*Y51KfZ}NWg7p)Uv>>-Z5#Wrz=gBPYR zzIX9kF~cd)(a}@5lk8aqva(r~0ajr@mGlq@Ar06 zrefBFYM;FnO^LCuMHX4ub&~0(GUsiJex!t!R7Tk;CNvPwXTXfkT2m zzWQfyv#IId^Lm888tn&6J@0x25a_-wjG()~?r-cNC$x8LBs-{vxKSvgg4_Lg{4 zvZ>&AGC6Sty2me1$k01C53k{n!pj{_s^x#QY6_4fyq*oy;e@hN&NNwGJrq>OP@zR* zQBMA_O>ZbGFq1=H{X-ioizHIf0`D}@?ife*p@8p6-*MKonYQENXUHfi!aQJGMVOJP zg=X>MCPRJRaW%u`e!1Vio|fM+UuN5R^-VNxy+gX7H)DXe;^yXTi7`z{QY*2hdh$vc zR4)4PEwU)&yM7UMeDue)$8-Tgo;up*3qJ@U#$$dVIBwQPyi4HhwYBpuj&wyBAx9pr@QqmI!4MkABd*p0U32sHisv zKih9x%~xDP+`wQo-4#TYaCMFFDk*MOXsw6GyEqAltoo&ZVv> zON;|)c;aZ|JSBaXn|YF+j37eqdUF_ zQk(&l+A1n54GLd~>N!`lJRXHDdGUyy3=b4kbaYKn7`L8%XdM0=X`x^g!lN_W~5JEj^a3!SbYrZ4&!~F##4Q#hR-!^|165>c*@`Nq8QA{F~ug}KL zEM}U2h8Gy6Fx)Xur8RUCpvlPn)MzIkFc!Z*bJuvEB(c50>m9wWQB3nf%hP=aQT$>` zVmq&0nK+BMu8%s#v8F2c$yAx#Mt{r>d+6cv3*|<>4Q4Qz$^!D?Yquv+DDoPW-WNCs zsblBsD=)9*@P=D#wV84e2L@1P9988zNrVZ1Pj-{?JOB2(h zhT*DTN0wKuOT}dizLaEgkcHE$Ltan%AV)>oNU-Lx`WTuu`zrezA2fTrWrI_;YTuL4 z)RaiN#aoJ7kKwYPO`bF|p&BoYH?=;kEn#$pJ_mh?z>0|bh>szNcU(z z@K3RhVr6r%vrM$h1p6?>^%q6L`Ske|Lq3^*cHR@B(2x+lri;7q&0yoRU&S3<3j1tN z|J*K|8_|nqPV^66<7th!OF%5N$eJ6FbEg=fsBNfzovlLcO4VmAeyf91=Z_c0 z=hPOgdUIvgV?F$RrO{$}Yo4mF?NY|2$rKYMHf@R8`O-o7sY}zJbth}n3tXP%^Fs^1 zT{W+jvRvxJlbZKueEG>#{%VrwWLbVV$m;Co1(B;oU+43_j;H2+J zyK0`d4BP&Of)tvJ#?x|Ih>G>UGQUmf(P;A?4j5yCKd~xQM++BrV(q3N?#Bc^IZF7w zmFdp*0q^ql(f4+1uhy$@>3tHn)l?l56w+>saN6LF3ff2(4BC*9rGWPqig73cPhDDa ztgL=duw;TQrq(%HxASKwoKCh2^I&CSHVzdE@ZLB$i=*g{ZZ%1V-P}nHO!3^}f7bDT z|M;L4cjcU<%Hd?Yu^&vYv_|ef{^ubD+%9P+`5$6{OcgkrZ$C7`G>N0cw{7WGbEE!Z z!qN#$4N9+`YgGF7U~p2Na&U@V>0D*_np!p1NK|s+%7wbzT~HXG*!{fiA=Hq3TIG;; z_s0wha##waoB9j~$Bp%F%ohscp^$2e!(`Vabw>cbx>L2BqfW+pGCiedGhr@s6J<^=mt3ySUooh|pK z+?GkpFpgnV#<%smB&|nR(xz)16@Bg>kwu{4RQEI)2XSdE4GFj3x<$tkIpQm_4tF6= z49DW`M#u+WjJCX#^R-bE4t2++o%GYLgoJlyfz8w<#s+U?;ADohgzfsrq|e|kzdgjj z?ml%nu}4Qq_8GdYds9A{K}bEwd6bMwvO z*)@D7hzr+WRT7=aBoO_jHqQW6!%}k`191Ve^ZV5$p}2j8i{|0CLMZ6-BcpIC10mC8 zW{Lxgi-t;2k==jxkGg&Z#}bV1k{vtso-$V zqom(o#w`wiJ?&GUzsmeC*Os zj;Jqb*l^gTuO3LZA309KL(&;om`dz*RtZF@Jb5FGS@7P{;F)hUd(bmhI;=z+I4vFo zc`00dfSFGjz`&?SNM@}t>sQ}L3sV9y?0U;z-;c&ZG@XXyTa3ubO#+w$-dtr3ZjaL# z5l{!+-Cjst*SUH5oi9ISIBSjUW4a)GeU%T^>zzE=S{f|-Ys~AW;fHGrCi@ zFX|AdS4gXc6a3~qjh-Hcu`UuL-xTxJHa&z)?-+`dPCbu~Qwl)DU2 z-SV-4lY&)Y>`$OTX(>IK%vJhv+{|23Zu0iqQ>Q27H%sR(&Llq&p79E!R znI-d8_5;KQiCJH5RNZO%)3{ELr3%sKV8{@ouCu#H{UC1jpvc=_RwW$I5&+2R| zt1xZ#2P?8JP0pJYWAA=@c4bTv*o9GpzwF#*zWdqI7L_u-7tu4`Zmp&pOkXVI&^-+= zxsP)%`D>LHMy_H3Je~(NsKw{F(iV#Mj~cbcXTxkRtWI;*Jm}w@h{ZL{`|VpxLWCpiLrh|z z>MVvj{<6~(>ItWL^>(24Dhm|#ml8aOP}ffbk2p^ht_J2>?)fO z)|!NM8Z|ZfGWFvA@5;3zw7VJMX|LX^YFV8Q_8qH8a~##U9)+O@l6DrErlWM5jl{>R zKvJ+u(#`>)-M9jsQY1Fi*L1;J%$hWG@4u^=v*r?*#NFtDtZ%#H8U41SASX$+iMjj` zg>E=o0;L&s5Ji2Ulh(Wx^_MtCBJrOtX#?2F;uoiSb(>w1|6yC8l@3+LdFDPqr_u<1A;1*8i=UbCmCy z{CJp0@?3eItM0~V9Mp)FGZm{GN{AqOV4Al&iOR?0BLO8{sdDK#HiFtNFs|>y>wFaKtr!P($(sYR8g^p4y?}^XJcN zsML-&#xS918gZfFmBaKGayl?dylkh?E{a88UF>BytAL*B(iy;DG=!0T21-+_=SRtW z7L_*Rx!TONQdxun9UZfaX9$6@hh>5b&OP{VRzE7830?NP6xc($)4B`xt0`$BjY>h?-41-7V>n>Y{)ipV_~y28M` z2&qw&N`lU$dWXBDh+DJN^|Pu=1ZjSe9ugsWHXkCs-^f&Cz1d9A!>g~-K`$3x(Lr*& zNvDxMai-7sg*2#z`QiJNC5~c4Uu_ph+9>wb2O;zr&$L>N53q}iePDQ)&XhsXkEHBa zf~KH$vvn`nd0I)&sZU!59G(GNHirg;bNm9KKLrj*ft!H4p>@q#52FqO2A+Ev8EOSD zZ}kAr6KHK};&gA7GMHR8lc^r|lVDlg>K3Ilt-!WiIjJjq=kg9Z#;By0!I#bCER7I+ zjL!V~GprL885qt1N#3py4Dx>rhmaN@Yafy^=*TpQVk6|(3$`hTw zmqg>jqctT=ujq+ZB<*^AB}QAs_>OD=kLE&uDPc6zf~t$2Eg(SnSmo$*qL}(-b0(%K zqwMW@oa=TG+b-;nWKackeO8zi;PZPe$)x^e@+e)#<-*H~7?)2sviYS}u&+w*#zjDO zVQS^@dUjr@j z9`b{h^BP!XyP=|JogPuShK06s9CGd?L1bHka(+@KkAeo$pA;6k4~&KHgne9bmz_B| zL0pRnB*DHuJKQw_`lbvuu_DR+#+A)so(|=Acpk9tJr})MRP;W+lV3FoEv>DEix8yp zTYId8nF{y2>*OZ2*6zv0@39tUd}3u04?YVu;`CImR;^B}IlygzsZ3y?zLA#wc!wKQ zW03vJg+HhTx_&gYb^^7(-^}7tTx>=WncvFW8TRoltdJ(?j3_s1xukg~ZmSUcV9WFJ z-RX$Bp!mx}q6VbODCM;1$9Cn3+9c(P*3zPqRk_K96vM5lp8*Wmc}Ll=(1JD~(v4Lb z6*-e^%Tgb?y%nrCfET$vD1^r^8Ki#>&sEaHCnk^>X-DDrzjQ~Wi;^^#(-AB)DYBJ} zaXVdJJ8hZlZHY*G9?VT{$ODRI0i0$(r&L7wPCMn=qRjrG%+llSSz3f)k9y@PN5 z(!pJG@NpcdcZvQ|$#=9@V7o|B)SCZJ@#SOwTw@fO2!T8JCnnJ2e` zv%J#5SCF_2f1`%Y;HbRg)<@!UGmn*()fs%L9a3dxr0GnF2waSYUDH7m^?N$tz^6X{ z{ODICOXf2<&!fmq<ML8!_5w4+wTMMLT4p<)U;9KvnxvXPMwdi$H9*LG7>}+gB&*eM5m+0^y zG9{^{vJP|^{}OAM#bX!yU-E}u-{=f%QZ%DbZXS=6Uph?j;wjZ5dOqu>7Mw6K@&r+* zS8*2Xs9Q-f&rb1I%plC%gV%PDt~T zl6j%8@gqsOgtbH6PTl4+M#RL{C^|30&==tp?^4@TY9_K&Gz?K;;)Q2&f#qJdWyQre zS#N%0i>q9`fpxB(v=gA5zcZtx6U8wN5rC%_SVHHL#C>z>*C}R{EQL&jpy-U1n;YXF zZ=(HeXlQ!~ad2;ZT*I;9HBxHw;4Eo?HEtgvkrHuBX|a0UV4K?W(!YN#lti`jd(P7( zOlO7KokUA~RzRV)=WG~yEqLs`M(!)hy(bcDAW=PwEU@K%VcA4MO3Lck;9F@y**EWK zqo;lMQdQJ4rhKP}SMVs(MuqfU!Drjl7R|{x=lC-M#@sP;N=yWzgV(S64*P#v02Mb| z3uDR=nID3CSd99?KU0wx1-7?&*f(^AjZ5ben(f_yb$-#YbQd6ScJqMQUk5e${f%{N zhw1vT@`T|;GKG=b$m7W>ivbkI;y}u=dd?DS;7ax5k+?jq>RZT$16QEmIAx;5vr0-# z>|a=a0~)SdY<|z#1H?aG=TdrPl(WCb+0~7K=9DVfDsa*@cg9Pv2Nq(Kco=SBcvRKi zKk}bHZ+NLP5&JOLX=lLzZe+Az+w~T`>#|?dV55BAa2~D^tk>i%3>`ajAi9%4Tp5>? zK=jM>u}rR%*ZH4cLqRZ9sfW2XL&s%dW)dGhethEL;Sou23HmJMcGf#g3pz*;C&(Kv zHCm0NO5L3gNSBhbkxrHyi5oh1q3=5`G`A6&KJyP@p~TBH*4J;|A7qN_{kt|y;$x%B zFyrA*8l+qJCUCyq0r_!E60`$NC3^(c@*ZPU{Y*nWNCbQRBLmaZhr`YE$n8@-aftmX zS1#13!}yvP8CG7ceR!-wy+=tfuCU|z<;#~T{BJD8!6~y;uXV!6D3JAv7dN-onzQZQ zmTM2xYwjkY;>q z5ht10?74SoOuE8kgFK~j0_{XFhSccnv1dtwLuNOkDnzpe_K5;}rHN%8Tz z-$qLn-ZZ~|EZ(tQRG5j=%O`a&^N$UC$IYJ8$BYa~_E9dKI9kcMiFpz=u$8_R6$KaA z#traFk%QME%E9qImDTN6z%MeZ_A=v^y+j3z55EVWc9;DAXz|GbsiSyIq4=fQ{K3V; z2|)_jIooJip3H%>6SuyS@q$H<6=0xLgjky|3xj znGk%9tdCI>`Gxj5B1}O85x8}BHItZ>B>WsqsG!fsOf9%T3-acR-wgo${C!`8Vy*`z z4>Yq8ZVnE6{SUX(GJ=muLM0YGKBT?p_AeJ_LtqO1HAHO*nq=PbT-nv&`6xo9pDdy`#q-Ww)tEOHk z4v!M{Y@}%e&do$N?wJFu1U8O%%*uT!Mq-6&Aa3VHC1*4NbZe7#|KTooEH|l6BFXtw!KJ19- zKuxs#(riSF7N%$dLMn2*ooxUjB&e_|uL1#HxMlBCr6tx1Sn?nO1;pG*f|2q!N5SCpbcA#I5t= zPI2)Ng}fOKp;&5xZIYFxr96Qlo8@?}e2iXLl-TA-hIo<#vYoHr$p>X~sZ)UZ1@Aj~ zqJX==^uW0G;~y{B7jO%z5iGtSyU{@ zqH9o^vN!i=P)Cg5Yu5dSr`quv8jET5A%8*d=8&-s@g&wuV^3s#MTVg5y-z=H8{*8b z=jK*KfEjlCF9BWPnC`&&6y+7q#e1^DtsF}pZn_bWHC)yVnz>Lx=eH|U0*sKN8 znWAESuWZ!I`iGf=(s&c>(@%o`jz39!?BsIL%29AYI@8+y#41R{w>l2R+2Xh=IPi09 z*OcH$IL*5dLv9__>M@Y-)IT2!x4?5vLh(%+BX;_u)cYNjDVlvqVd>XUpaZXZJ(1fm zpyO^t7bhpr=8NXVi=w>X@jS)w1`dB{Sw3#ybN%1)!g+`2mj?pi=CYzSQ!_FgsIQo6xKF^nCwn;rT5W#Foxj3f8S#RnIC)7zRiP46` zt8!W0EaGR&Kyb)N?DQ&z?FFb@-y1yE*h0_|>lKxHWr2|M3zle{Zh8TBWE<+M-oS?5 zZ!3%{6WkuJN}nGL4Gu;`E53p)_hT?h>NYi4t0}Xa#DJs(4Mec&MX=jjlDo0y$ImLi_v0cPwBqfdmJ65>#nA0JSzTAwe z9IwieWDoBsXn3=z5KqX=^d50-Zxz6$dfx=N*oW3%+KasL&Tu_@MdsEo7W!jYM@Pi5 z8Fbb$Ie5s*jAL7xJwdsTeg}--Yv1r^{Q_oAkngzIB&M<7VV_J?y|KI$1!QxyK9UFY zB|fDl9k;WumVO;w%yC5hm5Cm9f@N#1=jG?41#|>b2n?kAfP{SHlMoEHGMWC<-mitP ze4FD5KOhjzt=GYKFKR>U>wibP}xI6vorLXn< z-voTbdn-oAngLMx%Z zoDhM4y~i?|lH3iRSjWT!>ObFF@mG(fCZq*UeQQGfQvGh)DF_NGE{^MM2AlNcl^FFg zKx{;u9305Q%bg0QSva}8Mp&;Bhd@U$D>j)$9!VD>Fv_jz07L;lzmN^;atgnSbd3C^ zm0pa=s-fpa~_arq(`}1nu_Xq3BoAnfH6o5!e z^7CH}Z;s~~xG#9OEyCoq%2|hi$^LYrIlU@&@Yq4|NV;>Yt9$o-`z557A+5}AWYdNLMo?P(^R6xK=>5$CbZc!iJUG}{OiFPRE&W$t z+dnTNcI`r^kbzIdK<4txqs~o+1NCpKcW9q77-YV+E}RSke`%(|`i7qDh$YMK9sv7b zlQuxnB1(JGZN>*%D(ZG3dX``Df8?WH7*|k$ca;^v?H+${73@EcAx+^w`m{+h<^pSY z?51k>vk#1VV(6us;Q&C@?34cFg?hPH+%mkv0&xE98r$ul#2 z@&5LSx$GqM zb$G(tu4Qy+@x6;;xSZ#?z+vWgb)Sjp{mUDNE zNAi=(>y_Kixd2eRlFnnp<^R2?xm?X^J?0ywe~!%IBvt3TqEBgB*|u}KkmQ3Dj_8l4 zdUEa>@wb-zvWwS0%+CA79WF@-0_MT5%CmHk_0Kipl|vt3;GwIlkNtEaj%?t>1rd!< zkbs8~c$a@4emWgZ`S2vn%7SS4B4?BV?cT?@w8q{@f%_B*}7W&Q;TzT!!~SdYeyH| z0;h!cJ_?8Z7I^6AL=y9%Al@vGY$B0=f@*y;FAf80JzMm(?!^90{*Dt4PugkDWJYq| zcZkT>3R9lIbK=3}co)dS#)hpRN{ZL9)G-lxRYT|4aMggB9hgWmSQc8@^2e0-4P(#( zpW_aSDcsB~M=?6CmvwY&;(gG{oO9gy3qGk(ZTld>>~!nL%n|x-N2B5gg8B2F&-L8m zp72u6QyqkNnesZ*4{w&Y>_?Rz_27 zL8Fe_;VEMZgL=JT%RYe?kE`n*CBhBVeCvKHFj_lKNH_jFx$Yyk6sISCbg{VF0i zQOglw!zpXcwta*yAZ?zyK7V|c!WUD^0=hmJP#30Waj>!`Z+W)Nbc^=-WzYZNcMDPa z1KOyRl(+sJx&^V%6->qvujia2Ys~94Fc1ZEb90G)k5>IB1hbw`^?+~!>*REgh2e$D zy|-Xc+j3r<)V7k`FVxZm07-TRKQc|(xF)2eU?c0jii`E%-w=+|Z5>@)0?ObqTPFP+ z<%a8k5bvp8m>ELx8f)#nztqwLd5!jH-iHRS!>$AF>fgS7dkfg3$GK{;C$3F~bTd;^ z_<#+GOUH~9*Efl(cPPl55Rl)fp5lNOufz1RS~5Semet&QN$5t0my3e5u%}l$H&K`5q?kkfHC|b#VcRGO4;R-+9C8G z7VNu+LD*zz5`Z0=EC^Kwc6)>wmHXd0?6^)*fBqkaXR_Jv0LFGz^@2t*@ZdWmFT zm!^|NtJ^*y_p}|5`?e8RhnOl(X3IitZx@gHgVltg zYD6e`tp2{5#bW9yaIDEqoViA|n}>zvW| zW-dv&I2UQ4*>Y{t&h%RJ-aZI86CZz@D|~tD(2f_O>RqYBLyW=T(D;M3)ugvcV)-pD zw=Gdf75UOT0(-vk^J`&uG&J_+ak!xCq@%9Ri1b$(R0VoV{ZS;U@ z>=3AqUnnWn3l*!UEHUN=iur4gDcpB?0u2-CzmrhM*b17L4m0^0^64Z%XJvz3jYH_v~iV-`<(_;Z=dJ=Gz# zl#mVIB(nX{(EhQO&gg{&CD2p@zMoWFmkhxvVp*($?%1po!#gN0)a+c^n)1+;#VJnpzn%N zGB#CFYOMs7wYBaP0v)7^Y^_HTOtH_dTu;HENarcIPS?(fkiV9B0O#o1tN6E#I3p?6 znMI7Ay}v3y3i_XlBH)}C^;TG2!A6k{c@eMQMv|N9`a($|m zG<=eB)WFhbsdqOq_ljX$@Fi*(eRO>6);rlb^j8gqQhU?@C9BpOR;;d-)-erF5D*quu zk(FqHA8gCJM;Ea>KfVlK88y3$wiOu%0sISD-9B983@+bcVaa(Tf0Hr1CL2yb!#d50 zCZGmtC2S2&XR@5V0YxIb<@M2h>&H7V;xP$O!s4O$NYdX*THj(6^z^9Qjez#QLcp!H zXDB4%D@xAtE3RBe{UU~YpFQ{HB+17WOn_VVvLLt{1C!gRgE|J(pH)1sdV3`dAuSB| z*Ib+aem;z^b^awg@4b-$0xHa2&*Z~~={eYipA=a8&YhF`G+r}V?P8<%imD1C;Y-x- zmJSrbK2VF%I*lr_({>B1b1Rs-SQ^>So3V5 zQ+aXr3qj5nRgKn31qIAbS|=nVbRC@Oz#1QqI^ceouTQ)o%`)X31bx0<68Z0Rvrh<` zSyb>d z7%$)G0pVn+wmqNH{hfy>GB%G4Cu)_Uk5P*wD$j!)kY4@I!YLrf#$RFauOjO!w)#*UUIjUUu7J(=Qa8sOw zXz4RNMB@5S4+tJ%y#%hFp))%)AM!udoi%VL;W8;i4@Or}JOD^1H6`UaCcQTL)bw=y zLmu9%#?xqvpofw*AdBk|%hx!tayZGH7cbA?D$gje>t7h`=O$<|J9{OG+3C-v{BbN| z+&HZ-*J1VcR6&??;Qib;NKC|}j8EmLrQdN_gY=!9y}b^~*C^KuXOL#`1*!#2ZSF9$ z`EEp&ENxcq&Lx~JDwD>eE-__dn~Wg}cRvj8L#TE)u-qg*5yiKC7sW#O3Z8{n7kl3) zH?D31Y*I0Lc=us{8ox^7e`1jTaG%MDh#&%n0>w#PvMK@7D{#~+g1Y}g#-`+D6h^vp z_>E0y_pK5CnBPi_aQ7G$1qXF;aY6S}eAJw&IKX_EP+4GfADms8d@U=Jq(}LjGRxn9 zE#RgMhGj4@wG>5t5vbW?%&io!aHw}+l2_789(=>!^i6_QV#SuSe-CjzK&~ZUwasFQ z;xTKP)JH97q+*M~`}Y2qFGwMBPF;2Mwhw* z1T?9~7iXxy1GqAOdG}LU3wmE0)Y=gD$I|TdkBvU}oU6~+mhg=F8ovKc0%RYYJ9wG( zL(eYzru$EF^|53I1v2PsRZ&*~rvgfM09Ot13iLY|xf|U{1nVtfG=bT-FbE*$=&Za8 zeQ4;j_x>m57rX$Pp5NRyK-Z%E4&Ke>(paG=A0H<$JPpgPtgNIX=8DM0i$9oeaY=5sV)kQX_a&T}wWjB8t8pAXpDE(@^s*3atV1Ue*JA)nbt*MQg6Bd$B zq@0&K(;ww0bbsar$%`YV=+FwG*FQgO3wVD-=3W-$wrA|=PCybQOOEyvLWA5`kUE$A zKalgG#L;igyHV(mYcvwWKJ~O}Y{PzO^6Vc-5_q#?lCyJZ2t8%wZZp!<{8YQs3#RQ# zNQ4}V_ezO)=M~t_jHC!)etx84H+jU&YM(`hr_2jtuws~96|q$ zEixSW$Tja5aCjY){(8Wzqsw16PGQ%M)R`TS+XWdjM#g5Y-#?^3izXXn0VR@^#Zphe zM!sxH%R&3qvYd1l%=GyK6{bCm-@n4?%IOtMc^J17imCj=buphEjf!rh$6KpT2>w&- z1o3kG-kAbOGrT|{Q->5C04r%&#JW{WP5F)wyXAKcB?|tdEb|0gZS84K5T;Us6phUf z0y3TGOSKJ83Wan5wc_H;xt~XZR8&;p9K0*A&4c88k}j`GD%w*&$;D9#U>H|;(KBiv zMcC-FXYQR{_1tu`TE&vNz5D&z$R79EpZ7$_FIb`44nd(YL{?#Dlu58Z1C$YAMB@r7 z69iuMnu0@RzkpwXt4!R&Oz9lLQtmz!`-Qee0oH&qkxyPuZM!+(3RKcU4+DbS0Gk9D z-Q8kVCDBu_wiYWO0k{C|`IL}9oZG5z9ZG`%9l)U<^+y4LGVneL1)33hg6+Q5EJfDI z@o@=g@^}-xHvtECvsO(4xagddzcUXXpyGs?8Afcv7UNHLW(tkQMOHsN6*FDxbpLql z+OnWmG+5EYR~5hclu)vk0-EUeR(&~o?XhG0LY|?=vxbQO70cpm}J6%62 znp&`T5p+NQwnqRvf_Ht>%5ntKAwBTH*bUzyrQ|-IC&gFE39cHs6ZO zJQ6&W7;y?Xq6X@-2IBFTAD1vgx;8YY3PDpGtm5M7rTS~o2V}rgsZSiQ2kSst1htdm z@DcwUt8|1NYgqD!fYiD?U{PNqtpI4v$z`qRYaBuOlW3`z_Ydam^GVFpeJ8%>A}5F| zljPIjV7YEwU|V3x12!#4j3&x957qB+fZ$9=W;6Q5jf2%hq1pfUf?Y8pQ>732wOK;V) z>)(`GtW&^R2~0B_kS$v9vKaOAc}R?STti0I+y;W~r9YQ@q9y0f98{CXI+BLp^DKp; z$YrJ9flY6k_@suz-9)4vFVY-N@2Ajz(67@A`^p`b=08*WN?2R zoCVIt-)d-({;VG%K9Gospsmm;fc|&`oNo(4ASW%}VbBkJuFY3iaW&BK20H=k6l(m& z8_l9Qg+cR$G_`<6N$!B*)H~k;aqT-Hxbd#zANtbVJB(U7pp^wMV$+_nDq&{F#}|G! z#ned9md+I9$FBT9FtbqvfczE7Z(km?AiDH`U-%FiqSZhnelRi<&uYIHF~An(+BCs4 zY2t7*#~W=?sGM@o82^PQKdYif{6Xd2X;lS4RvtqWYSL|SA#ZIgUZD6ts;;iC1fyY; z8<1_EA{Z@9cA@XXywO75LMr1>>`{s>QKiD}hgKMVXYIxpWto%9P@PMMKnj!Tsakkq1Q zqXaGSMpKY*=@`xMU^p`ck&wKx77bbh63Fp`Gai*}KfRd~ekh!=mj6Qp-K`j8!h2L$ zV5|Dv#h@iKVqb1|FGd{Jifzg!5ZI5{iLgnt)Jq&*(*z`ds$3z_xN@)8-8C?S6c1

5;$bXLw3d*Gtieg8Ka{2#=mpChNxj$S~@yc|C9_T`bnH<*`Nm;{_eA{cUlTa z0sORvM*MHqsBcA(Y7F9Ox*u;4UCPB1KomhiAoQhhYC9n>wC4qh94(0AakgRDlG+Jr zfy9Q}=sOByk+qW<6E)vL6**90+Y0N6AW_fETVreQ>$^VgxVv#m_dlIgQ3Ch1g_6x1 zJD+QuO5i4Cx(Ls)D(rJ9!1LOs#-Y5?nmKt6m3!gc4nDo5ff=GN7Z2W! zthqj()Inm6R6C*lc`R@zFni1%#m_kUP~G4A+16^h1M&zL7dLH{>L3`CHfU^Pm^t?l z{}}h9n)Pk_uUee5`jhk(X@lkO|YN=co`YM4D*j=|7SOpAcvy zOFGcN%BsK%OeS?l@C~efEfyolF*P0#`rCBCe{^lidmfd`YCtMqMZs;@=wE`FuNc=@e<&j_-ECW$qIA~+#!Udo4znvu`y9FX|*J!`Js$+J)8jG#%O@Ix2` zN_YWhY7l87E^r_-Xlc$RFvIWmA!7HLS8{Rj+X#`cCpqjOa`|H8VMfqAVpp3~ zo;mjvk&d6;25vJonzPKP#nU1n;C4-}cA2 zSNli-w=o&|N)o>t{Bbrm_76#;L}+Bpq9 z3qkY{!jW;vU5X~TiGrBRAsWR_Z#p3=H{A|-iUEQ8bA63?NB}e@{a9SgJzUKnF^HPo zmdhFrG-{rVYwKsATFbB9Eu~3YBOWj~aMZ8Nx!Ff%R*&R0w5$O8N6?Wc5F}F^O$c4R zUgO7Gi3~=?9Ho+YQ&|YqPuAiFf@6v+hO8B6e+UGm?UAQ*&EVRbbQoL`k zyHJ9fo11+BbJlLUUIu{<0A-eXj}eB2tV&SOUc4D22|hdVN2W9!ftK{!u@M9};16SS zGx39fhVt)27Wg#((HfQm)gmlADy4)>9vdU%36if;tI%Xf3tk6Y^Bx?&=cqR={%yKW zKw$B?6x%6+wsE7% zSyA7ADGzIilyDBuX=`iO<$7QO=t{pOP5IX#VdPhxK@l&@y%M)eL(+-z*K zpQHH2KfIBkEiNj0ZlC&lPr!-be$#-|FRm9c>RR`(rM^Y3KAETukdq5DyA}bRTPBeO z&z|QK`$3Gagq|YT0sg!X)MTniU*P%FfjV!s`h|9+Q|WaqopgKN@Y=Sr+#c*r#9+*K z<+c%ohg@xVikG&Ij+1e%&BugtAJAQj&uJ zYW1%>4rqGZP!VFwDPfZ`+v3C`uQ69jt#4&$tfhDR>Dk*4s`02?8*JO0NX_u!PYNI3 zhdk^^Ul(M#@(W{id&_$25W7V^y$AgT7M zw)XUikOSr^@)_0NC+DMb>GuS!7xiAr;KBGD_20a!l#G35`=0MfJ9n|J0mD7|eCjXE z4R<1RPlgzXXNg1mkGVkUUh-@UA*tpJ{p!6|6*GZ2B5b0$=KmJxd@ogAs-*2G)9o>p znCq{;T{#}ATe^KLtP?8pzmlOGQ8sFRppqHBi~dk@AOsV*#e#&j2MIWz{IB%HYHdJm z5!-&Pq2VKEpq`eV@uSB)f)o$s|E$^b$Vd)Tq$V2!AZaS1Ua>_TsG2_ov$0;aWjq-0P7EuUwz&Syms5ew90PRZI z@b~-e91nRcNleznX;pvU9mM&!SW^}?`G%lxU*?Z4O8e`ZdQ4ibV$O~yc5o5(|No8F zrgdLZczj?VLb1Vd2bG5up17o*{+TC%{eKm7-!;7nIuW$~8d^=K20uk`p$0Kz$l-47 z&|ePgv#q$?A=e_U|69C7#Iec_Z;hgfGE)7P4q2}Tt1Ka7zB|kv`uh>waB}mlVv+K% zAV%x|Rtw+%X5oLCCq%iM{Z&acXp~E&2oX&T)@|%X88#QX9_+uxHxFM<-$iUmmmw|D zQ5I6XwQ*I}$$$FkYFJ(z5w5L1|ANhh@xSjr`}8}#)l5zcW%)b#NBZ?}u-t9L(7vnG z+wYnm$o?&{3Vm2r!@roKtsxZ@+yVV`%SI4DCg=_&N3}zB!rP-s=6+aQ`2Tl7hPYK4 z*?P+bwB_J+0m*Z>G#9GZ#=`z9VgX;h{$Dlc`PK9l#_=Br1eB#HR%HYX#eocq3Mg#CPYRO!nO%Z8I4GQAQ&K#VZsc-^oPn{ z&{w@L?wfng_ul7u?mg#@=W_#k-aK=lhnkRLCk&FaC!uUbPLC-y z#5|F1EPMxYe<#bPWp_~Nw?v(RQk66_0t6732fM-RyXW;dwty3ucXrol0eB|eQW|8rOU_&N14n?vR4clsrta{2@(Le=B<&_2Um}k6F`}dz zZl`d8*@D@{ybT>)ai1PZ)|qL6WLv6DO&bWD%-Po^r4)sygPRF^ z(_JNn)dZ>{ut%hw1cHK(+P;FicO?;vw``}W-4=)>F|@A0;BR-ySO^ZHUa;Em_rA)I~CM>_YfW4-Y9_)Yh749#0qvewku$=2 zIO0Wp?E;&+#2&``g&Tw}@9zzf0)Tt1K8tjl{&8yF<kY^Wtc!0)TSHt*DJHM$cPQ)-k<_*#~l(V@1k7;M-gkZP*d6eKtyj zIxWOq+;Cd1W>Si+&=YN)tjqohS0YVpM#j}!V4HYZ0ML-z0{<9v(Rx#F9d`U-v}3ys zk18btBu<&3Opz1r2y_sgbt+-Xyua6oz0MW|BnN`WJWdt=4AKQbD|P)81C`CT8a$-r zt#@U+A$Qh%)aIgK@(vOcOruch1@3x+VG(L zdUTD*SBcs12xeqpxVr-YZ@BiZH6I>Y8i&R6#g@N(0~%!UP780hm*&V9>$5OMO1nTc zC$^_%S{0ZRmX@yq-EzDTD+M%!(je*wL%c2`#*hCgVayn=ZF->sT8u)4}3oE`E)G{O#i9@-EV%*EYW zagVO5B$@bTrEUAObkXNb)nxxq#)$xMqhm!K>$S>U3-Mq+EHknEEHr+{nS^e1RsLYD zF%=n~;CkpOwazeIA$L$3073}a?q$`tV$R_)vDcz3RNJN-q@{s#^DDUdA2Q3)+UU&p z*~HhHO3B7LHK4vn0;Lw~#O30BR-+`yuJ@#|-pM}}+;ecRq<|{&jC*nS!5ZayPi8ai z04-Zgq*#i*BcA9bD&U5}zIb8bGh`T}lfcKs35L+5CxZ2CEb2M@FcmKe{B8_eE8nsm zqsZt;{&4a4k-Q!^1VTG^V4q|*Oi^q86nPLm;#FJG8t@DZo9WFA@21j8AJ^kq^xd9< zULs}lM&|G4*6Yk|iRUa$rNK)`w^&ngr-dbU8lq+~A8@)40xxYHBnkIi^)~};X*pu| zHoQY#nA)vGE@bz^C z3*4K{tAX%Jb#66gzWGy8ULH^AQsnxZK%yZS^<>ts5jP7@2;u$dat;TTpkP#9dSj7( zBdDq5Bz!PVXlW^|D61x#kNd^XF_rv5&-~}Y2^2$g-dbVXjnvrNVlEDD_BFPFxBmt1 CZ5!|a literal 0 HcmV?d00001 diff --git a/apps/web/modules/auth/signup/page.tsx b/apps/web/modules/auth/signup/page.tsx index 4a33751d11..119fd518ce 100644 --- a/apps/web/modules/auth/signup/page.tsx +++ b/apps/web/modules/auth/signup/page.tsx @@ -56,7 +56,7 @@ export const SignupPage = async ({ searchParams: searchParamsProps }) => { const emailFromSearchParams = searchParams["email"]; return ( -

+
{ - const freePlan: TPricingPlan = { + const hobbyPlan: TPricingPlan = { id: "free", - name: t("environments.settings.billing.free"), + name: "Hobby", featured: false, - description: t("environments.settings.billing.free_description"), - price: { monthly: "$0", yearly: "$0" }, - mainFeatures: [ - t("environments.settings.billing.unlimited_surveys"), - t("environments.settings.billing.1000_monthly_responses"), - t("environments.settings.billing.2000_contacts"), - t("environments.settings.billing.1_workspace"), - t("environments.settings.billing.unlimited_team_members"), - t("environments.settings.billing.link_surveys"), - t("environments.settings.billing.website_surveys"), - t("environments.settings.billing.app_surveys"), - t("environments.settings.billing.ios_android_sdks"), - t("environments.settings.billing.email_embedded_surveys"), - t("environments.settings.billing.logic_jumps_hidden_fields_recurring_surveys"), - t("environments.settings.billing.api_webhooks"), - t("environments.settings.billing.all_integrations"), - t("environments.settings.billing.hosted_in_frankfurt") + " 🇪🇺", + description: "Unlimited Surveys, Team Members, and more.", + price: { monthly: "Start free", yearly: "Start free" }, + usageLimits: [ + { label: "Workspaces", value: "1" }, + { label: "Responses per month", value: "250" }, + ], + features: [ + "Link-based Surveys", + "In-product Surveys", + "All Question Types", + "Multi-language Surveys incl. RTL", + "Conditional Logic", + "Hidden Fields", + "Partial Responses", + "Recall Information", + "Multi-media Backgrounds", + "File Uploads", + "Single-use Links", + "Hosted in Frankfurt 🇪🇺", ], }; - const startupPlan: TPricingPlan = { - id: "startup", - name: t("environments.settings.billing.startup"), + const proPlan: TPricingPlan = { + id: "pro", + name: "Pro", featured: true, CTA: t("common.start_free_trial"), - description: t("environments.settings.billing.startup_description"), - price: { monthly: "$49", yearly: "$490" }, - mainFeatures: [ - t("environments.settings.billing.everything_in_free"), - t("environments.settings.billing.5000_monthly_responses"), - t("environments.settings.billing.7500_contacts"), - t("environments.settings.billing.3_workspaces"), - t("environments.settings.billing.remove_branding"), - t("environments.settings.billing.attribute_based_targeting"), + description: "Everything in Free with additional features.", + price: { monthly: "$89", yearly: "$74" }, + usageLimits: [ + { label: "Workspaces", value: "3" }, + { label: "Responses per month", value: "2,000", overage: true }, + { label: "Identified Contacts per month", value: "5,000", overage: true }, + ], + features: [ + "Everything in Free", + "Unlimited Seats", + "Hide Formbricks Branding", + "Respondent Identification", + "Contact & Segment Management", + "Attribute-based Segmentation", + "iOS & Android SDKs", + "Email Follow-ups", + "Custom Webhooks", + "All Integrations", ], }; - const customPlan: TPricingPlan = { - id: "custom", - name: t("environments.settings.billing.custom"), + const scalePlan: TPricingPlan = { + id: "scale", + name: "Scale", featured: false, - CTA: t("common.request_pricing"), - description: t("environments.settings.billing.enterprise_description"), - price: { - monthly: t("environments.settings.billing.custom"), - yearly: t("environments.settings.billing.custom"), - }, - mainFeatures: [ - t("environments.settings.billing.everything_in_startup"), - t("environments.settings.billing.email_follow_ups"), - t("environments.settings.billing.custom_response_limit"), - t("environments.settings.billing.custom_contacts_limit"), - t("environments.settings.billing.custom_workspace_limit"), - t("environments.settings.billing.team_access_roles"), - t("environments.workspace.languages.multi_language_surveys"), - t("environments.settings.billing.uptime_sla_99"), - t("environments.settings.billing.premium_support_with_slas"), + CTA: t("common.start_free_trial"), + description: "Advanced features for scaling your business.", + price: { monthly: "$390", yearly: "$325" }, + usageLimits: [ + { label: "Workspaces", value: "5" }, + { label: "Responses per month", value: "5,000", overage: true }, + { label: "Identified Contacts per month", value: "10,000", overage: true }, ], - href: "https://formbricks.com/custom-plan?source=billingView", + features: [ + "Everything in Pro", + "Teams & Access Roles", + "Full API Access", + "Quota Management", + "Two-Factor Auth", + "Spam Protection (ReCaptcha)", + ], + addons: ["SSO Enforcement", "Custom SSO", "Hosting in USA 🇺🇸", "SOC-2 Verification"], }; return { - plans: [freePlan, startupPlan, customPlan], + plans: [hobbyPlan, proPlan, scalePlan], }; }; diff --git a/apps/web/modules/ee/billing/components/overage-card.tsx b/apps/web/modules/ee/billing/components/overage-card.tsx new file mode 100644 index 0000000000..479b16de54 --- /dev/null +++ b/apps/web/modules/ee/billing/components/overage-card.tsx @@ -0,0 +1,292 @@ +"use client"; + +import { PencilIcon, TrashIcon } from "lucide-react"; +import { useState } from "react"; +import { cn } from "@/lib/cn"; +import { Button } from "@/modules/ui/components/button"; +import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal"; +import { Input } from "@/modules/ui/components/input"; +import { Label } from "@/modules/ui/components/label"; +import { TabToggle } from "@/modules/ui/components/tab-toggle"; +import { SettingsId } from "./settings-id"; + +type OverageMode = "allow" | "blocked"; + +interface OverageUsage { + responses: number; + responseCost: number; + contacts: number; + contactsCost: number; +} + +interface OverageCardProps { + currentMode: OverageMode; + spendingLimit: number | null; + overageUsage: OverageUsage; + onModeChange: (mode: OverageMode) => void | Promise; + onSpendingLimitChange: (limit: number | null) => void | Promise; +} + +const OVERAGE_MODE_CONFIG: Record< + OverageMode, + { + label: string; + tooltip: string; + } +> = { + allow: { + label: "Allow", + tooltip: + "You're currently allowing additional responses over your included response volumes. This is good to keep your surveys running and contacts identified.", + }, + blocked: { + label: "Blocked", + tooltip: + "Overage is blocked. When you reach your included limits, surveys will stop collecting responses and contacts won't be identified until the next billing cycle.", + }, +}; + +const MIN_SPENDING_LIMIT = 10; + +export const OverageCard = ({ + currentMode, + spendingLimit, + overageUsage, + onModeChange, + onSpendingLimitChange, +}: OverageCardProps) => { + const [isChanging, setIsChanging] = useState(false); + const [isEditingLimit, setIsEditingLimit] = useState(false); + const [limitInput, setLimitInput] = useState(spendingLimit?.toString() ?? ""); + const [limitError, setLimitError] = useState(null); + const [showBlockConfirmation, setShowBlockConfirmation] = useState(false); + + const totalOverageCost = overageUsage.responseCost + overageUsage.contactsCost; + + const handleModeChange = (newMode: OverageMode) => { + if (newMode === currentMode) return; + + // Show confirmation when switching to blocked with outstanding overage + if (newMode === "blocked" && currentMode === "allow" && totalOverageCost > 0) { + setShowBlockConfirmation(true); + return; + } + + executeModeChange(newMode); + }; + + const executeModeChange = (newMode: OverageMode) => { + setIsChanging(true); + Promise.resolve(onModeChange(newMode)).finally(() => { + setIsChanging(false); + setShowBlockConfirmation(false); + }); + }; + + const handleSaveLimit = () => { + const value = Number.parseFloat(limitInput); + + if (limitInput === "" || Number.isNaN(value)) { + setLimitError("Please enter a valid amount"); + return; + } + + if (value < MIN_SPENDING_LIMIT) { + setLimitError(`Minimum spending limit is $${MIN_SPENDING_LIMIT}`); + return; + } + + setLimitError(null); + setIsChanging(true); + Promise.resolve(onSpendingLimitChange(value)).finally(() => { + setIsChanging(false); + setIsEditingLimit(false); + }); + }; + + const handleRemoveLimit = () => { + setIsChanging(true); + Promise.resolve(onSpendingLimitChange(null)).finally(() => { + setIsChanging(false); + setLimitInput(""); + }); + }; + + const handleCancelEdit = () => { + setIsEditingLimit(false); + setLimitInput(spendingLimit?.toString() ?? ""); + setLimitError(null); + }; + + const handleStartEdit = () => { + setLimitInput(spendingLimit?.toString() ?? ""); + setIsEditingLimit(true); + }; + + return ( +
+
+ + +
+ handleModeChange(value as OverageMode)} + disabled={isChanging} + /> +
+
+ + {/* Overage Usage Meters (only when Allow mode) */} + {currentMode === "allow" && ( +
+ {/* Responses Overage */} +
+
+

Responses

+ + Overage + +
+

+ {overageUsage.responses.toLocaleString()} +

+

${overageUsage.responseCost.toFixed(2)} this month

+
+ + {/* Contacts Overage */} +
+
+

Identified Contacts

+ + Overage + +
+

+ {overageUsage.contacts.toLocaleString()} +

+

${overageUsage.contactsCost.toFixed(2)} this month

+
+
+ )} + + {/* Total Overage Cost & Spending Limit Section (only when Allow mode) */} + {currentMode === "allow" && ( +
+ {/* Spending Stats Header */} +
+ + {spendingLimit && ( + + )} +
+ + {/* Spending Limit Progress Bar (if limit is set) */} + {spendingLimit && ( +
+
+
0.9 ? "bg-red-500" : "bg-teal-500" + )} + style={{ width: `${Math.min((totalOverageCost / spendingLimit) * 100, 100)}%` }} + /> +
+

+ {((totalOverageCost / spendingLimit) * 100).toFixed(0)}% of spending limit used +

+
+ )} + + {/* Action Buttons */} +
+ {isEditingLimit && ( +
+ +
+
+ $ + { + setLimitInput(e.target.value); + setLimitError(null); + }} + placeholder={`${MIN_SPENDING_LIMIT}`} + className="pl-7" + isInvalid={!!limitError} + /> +
+ + +
+ {limitError &&

{limitError}

} +
+ )} + + {!isEditingLimit && spendingLimit && ( +
+ + +
+ )} + + {!isEditingLimit && !spendingLimit && ( + + )} +
+
+ )} + + {/* Confirmation Modal for blocking with outstanding overage */} + executeModeChange("blocked")} + /> +
+ ); +}; diff --git a/apps/web/modules/ee/billing/components/pricing-card.tsx b/apps/web/modules/ee/billing/components/pricing-card.tsx index 43624a8a4e..00d05f1625 100644 --- a/apps/web/modules/ee/billing/components/pricing-card.tsx +++ b/apps/web/modules/ee/billing/components/pricing-card.tsx @@ -1,11 +1,9 @@ "use client"; -import { CheckIcon } from "lucide-react"; +import { CheckIcon, PlusIcon } from "lucide-react"; import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; import { TOrganization, TOrganizationBillingPeriod } from "@formbricks/types/organizations"; import { cn } from "@/lib/cn"; -import { Badge } from "@/modules/ui/components/badge"; import { Button } from "@/modules/ui/components/button"; import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal"; import { TPricingPlan } from "../api/lib/constants"; @@ -16,11 +14,8 @@ interface PricingCardProps { organization: TOrganization; onUpgrade: () => Promise; onManageSubscription: () => Promise; - projectFeatureKeys: { - FREE: string; - STARTUP: string; - CUSTOM: string; - }; + isTrialActive?: boolean; + currentPlan: string; } export const PricingCard = ({ @@ -29,178 +24,246 @@ export const PricingCard = ({ onUpgrade, onManageSubscription, organization, - projectFeatureKeys, + isTrialActive = false, + currentPlan, }: PricingCardProps) => { - const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [contactModalOpen, setContactModalOpen] = useState(false); - const displayPrice = (() => { - if (plan.id === projectFeatureKeys.CUSTOM) { - return plan.price.monthly; - } - return planPeriod === "monthly" ? plan.price.monthly : plan.price.yearly; - })(); + const displayPrice = planPeriod === "monthly" ? plan.price.monthly : plan.price.yearly; + const isMonetaryPrice = displayPrice.startsWith("$"); const isCurrentPlan = useMemo(() => { - if (organization.billing.plan === projectFeatureKeys.FREE && plan.id === projectFeatureKeys.FREE) { - return true; - } + if (currentPlan === "free" && plan.id === "free") return true; + if (currentPlan === "pro" && plan.id === "pro") return true; + if (currentPlan === "scale" && plan.id === "scale") return true; + return false; + }, [currentPlan, plan.id]); - if (organization.billing.plan === projectFeatureKeys.CUSTOM && plan.id === projectFeatureKeys.CUSTOM) { - return true; - } + const hasActiveSubscription = + !!organization.billing.stripeCustomerId && isCurrentPlan && plan.id !== "free"; - return organization.billing.plan === plan.id && organization.billing.period === planPeriod; - }, [ - organization.billing.period, - organization.billing.plan, - plan.id, - planPeriod, - projectFeatureKeys.CUSTOM, - projectFeatureKeys.FREE, - ]); + // Check if this is the "other" paid plan (for Change plan button) + const isOtherPaidPlan = + (currentPlan === "pro" && plan.id === "scale") || (currentPlan === "scale" && plan.id === "pro"); const CTAButton = useMemo(() => { - if (isCurrentPlan) { - return null; - } - - if (plan.id === projectFeatureKeys.CUSTOM) { + // Trial state: show "Subscribe now" to convert + if (isTrialActive && plan.id === "scale") { return ( ); } - if (plan.id === projectFeatureKeys.STARTUP) { - if (organization.billing.plan === projectFeatureKeys.FREE) { - return ( - - ); - } - + // Current paid plan with subscription + if (hasActiveSubscription) { return ( ); } + // Current plan (free/hobby) + if (isCurrentPlan && plan.id === "free") { + return ( + + ); + } + + // Free plan for non-free users - cannot downgrade + if (plan.id === "free" && currentPlan !== "free") { + return ( + + ); + } + + // If user is on a paid plan and this is the other paid plan, show "Change plan" + if (isOtherPaidPlan) { + return ( + + ); + } + + // User is on Hobby, show "Upgrade" for paid plans + if (currentPlan === "free" && (plan.id === "pro" || plan.id === "scale")) { + return ( + + ); + } + + // Default fallback + return ( + + ); + }, [ + currentPlan, + hasActiveSubscription, + isCurrentPlan, + isOtherPaidPlan, + isTrialActive, + loading, + onManageSubscription, + onUpgrade, + plan.featured, + plan.id, + ]); + + // Determine badge to show + const getBadge = () => { + // Active trial badge + if (isTrialActive && plan.id === "scale") { + return ( + + Active trial + + ); + } + + // Current plan badge (for paid plans) + if (isCurrentPlan && plan.id !== "free") { + return ( + + Current plan + + ); + } + + // Only show "Most popular" if user is on Hobby plan + if (plan.featured && currentPlan === "free" && !isCurrentPlan) { + return ( + + Most popular + + ); + } + return null; - }, [ - isCurrentPlan, - loading, - onUpgrade, - organization.billing.plan, - plan.CTA, - plan.featured, - plan.href, - plan.id, - projectFeatureKeys.CUSTOM, - projectFeatureKeys.FREE, - projectFeatureKeys.STARTUP, - t, - ]); + }; + + // Highlight the current plan card or the featured card for Hobby users + const shouldHighlight = isCurrentPlan || (plan.featured && currentPlan === "free"); return (
-
-
-

- {plan.name} -

- {isCurrentPlan && ( - - )} + {/* Header */} +
+
+

{plan.name}

+ {getBadge()}
-
-
-

- {displayPrice} -

- {plan.id !== projectFeatureKeys.CUSTOM && ( -
-

- / {planPeriod === "monthly" ? "Month" : "Year"} -

+ + {/* Price */} +
+ {displayPrice} + {isMonetaryPrice && / Month} +
+ + {/* CTA Button */} +
{CTAButton}
+
+ + {/* Usage Limits */} +
+

Usage

+
+ {plan.usageLimits.map((limit) => ( +
+
+ {limit.label} + {limit.overage && Overage billing available}
- )} -
- - {CTAButton} - - {plan.id !== projectFeatureKeys.FREE && isCurrentPlan && ( - - )} + {limit.value} +
+ ))}
-
-
    - {plan.mainFeatures.map((mainFeature) => ( -
  • -
+ + {/* Features */} +
+

Features

+
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+ + {/* Addons (if any) */} + {plan.addons && plan.addons.length > 0 && ( +
+

Available Add-ons

+
    + {plan.addons.map((addon) => ( +
  • + + {addon}
  • ))}
-
+ )} = { + trial: { + plan: "scale", + displayName: "Scale (Trial)", + limits: { responses: 5000, miu: 10000, projects: 5 }, + hasStripeCustomer: false, + trialEndsAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14 days from now + billingPeriod: "monthly", + }, + hobby: { + plan: "free", + displayName: "Hobby", + limits: { responses: 500, miu: 1250, projects: 1 }, + hasStripeCustomer: false, + trialEndsAt: null, + billingPeriod: "monthly", + }, + pro: { + plan: "pro", + displayName: "Pro", + limits: { responses: 1000, miu: 2500, projects: 3 }, + hasStripeCustomer: true, + trialEndsAt: null, + billingPeriod: "monthly", + }, + scale: { + plan: "scale", + displayName: "Scale", + limits: { responses: 5000, miu: 10000, projects: 5 }, + hasStripeCustomer: true, + trialEndsAt: null, + billingPeriod: "monthly", + }, +}; interface PricingTableProps { organization: TOrganization; @@ -23,11 +85,6 @@ interface PricingTableProps { STARTUP_MAY25_MONTHLY: string; STARTUP_MAY25_YEARLY: string; }; - projectFeatureKeys: { - FREE: string; - STARTUP: string; - CUSTOM: string; - }; hasBillingRights: boolean; } @@ -35,7 +92,6 @@ export const PricingTable = ({ environmentId, organization, peopleCount, - projectFeatureKeys, responseCount, projectCount, stripePriceLookupKeys, @@ -46,8 +102,17 @@ export const PricingTable = ({ organization.billing.period ?? "monthly" ); - const handleMonthlyToggle = (period: TOrganizationBillingPeriod) => { - setPlanPeriod(period); + // Demo mode state + const [demoPlan, setDemoPlan] = useState(null); + const [demoOverageMode, setDemoOverageMode] = useState<"allow" | "blocked">("allow"); + const [demoSpendingLimit, setDemoSpendingLimit] = useState(null); + + // Demo overage usage (simulated values for demo) + const demoOverageUsage = { + responses: 150, + responseCost: 12, // $0.08/response for Pro + contacts: 320, + contactsCost: 12.8, // $0.04/contact for Pro }; const router = useRouter(); @@ -65,7 +130,48 @@ export const PricingTable = ({ checkSubscriptionStatus(); }, [organization.id]); + // Get effective values based on demo mode + const demoConfig = demoPlan ? DEMO_PLAN_CONFIGS[demoPlan] : null; + const effectivePlan = demoConfig?.plan ?? organization.billing.plan; + const effectiveDisplayName = demoConfig?.displayName ?? getPlanDisplayName(organization.billing.plan); + const effectiveResponsesLimit = + demoConfig?.limits.responses ?? organization.billing.limits.monthly.responses; + const effectiveMiuLimit = demoConfig?.limits.miu ?? organization.billing.limits.monthly.miu; + const effectiveProjectsLimit = demoConfig?.limits.projects ?? organization.billing.limits.projects; + const effectiveHasStripeCustomer = demoConfig?.hasStripeCustomer ?? !!organization.billing.stripeCustomerId; + const effectiveTrialEndsAt = demoConfig?.trialEndsAt ?? null; + const effectiveBillingPeriod = demoConfig?.billingPeriod ?? organization.billing.period ?? "monthly"; + + // Determine if user is on a paid plan + const isOnPaidPlan = effectivePlan === "pro" || effectivePlan === "scale"; + const isOnMonthlyBilling = effectiveBillingPeriod === "monthly" && isOnPaidPlan; + + // Create a mock organization for the pricing cards when in demo mode + const effectiveOrganization: TOrganization = demoPlan + ? { + ...organization, + billing: { + ...organization.billing, + plan: effectivePlan, + period: effectiveBillingPeriod, + stripeCustomerId: effectiveHasStripeCustomer ? "demo_stripe_id" : null, + limits: { + ...organization.billing.limits, + monthly: { + responses: effectiveResponsesLimit, + miu: effectiveMiuLimit, + }, + projects: effectiveProjectsLimit, + }, + }, + } + : organization; + const openCustomerPortal = async () => { + if (demoPlan) { + toast.success("Demo: Would open Stripe Customer Portal"); + return; + } const manageSubscriptionResponse = await manageSubscriptionAction({ environmentId, }); @@ -74,7 +180,11 @@ export const PricingTable = ({ } }; - const upgradePlan = async (priceLookupKey) => { + const upgradePlan = async (priceLookupKey: string) => { + if (demoPlan) { + toast.success(`Demo: Would upgrade to ${priceLookupKey}`); + return; + } try { const upgradePlanResponse = await upgradePlanAction({ environmentId, @@ -107,7 +217,8 @@ export const PricingTable = ({ }; const onUpgrade = async (planId: string) => { - if (planId === "startup") { + // Map new plan IDs to existing Stripe keys + if (planId === "pro") { await upgradePlan( planPeriod === "monthly" ? stripePriceLookupKeys.STARTUP_MAY25_MONTHLY @@ -116,8 +227,13 @@ export const PricingTable = ({ return; } - if (planId === "custom") { - window.location.href = "https://formbricks.com/custom-plan?source=billingView"; + if (planId === "scale") { + if (demoPlan) { + toast.success("Demo: Would redirect to Scale plan signup"); + return; + } + // Scale plan redirects to custom plan page for now + globalThis.location.href = "https://formbricks.com/custom-plan?source=billingView"; return; } @@ -126,177 +242,274 @@ export const PricingTable = ({ } }; - const responsesUnlimitedCheck = - organization.billing.plan === "custom" && organization.billing.limits.monthly.responses === null; - const peopleUnlimitedCheck = - organization.billing.plan === "custom" && organization.billing.limits.monthly.miu === null; - const projectsUnlimitedCheck = - organization.billing.plan === "custom" && organization.billing.limits.projects === null; + const handleUpgradeToAnnual = async () => { + if (demoPlan) { + toast.success("Demo: Would upgrade to annual billing"); + return; + } + await openCustomerPortal(); + }; return ( -
-
-
-
-

- {t("environments.settings.billing.current_plan")}:{" "} - {organization.billing.plan} - {cancellingOn && ( - - )} -

+
+ {/* Demo Mode Toggle */} +
+
+ Demo Mode + — Preview different plan states +
+
+ {(["trial", "hobby", "pro", "scale"] as DemoPlanState[]).map((plan) => ( + + ))} + {demoPlan && ( + + )} +
+
- {organization.billing.stripeCustomerId && organization.billing.plan === "free" && ( -
- + {/* Your Plan Status */} + { + void openCustomerPortal(); + }, + variant: "secondary", + } + : undefined + }> +
+ + {effectiveTrialEndsAt && ( + + )} + {cancellingOn && !demoPlan && ( + + )} +
+ + {/* Usage Stats */} +
+
+

Responses this month

+

+ {responseCount.toLocaleString()} + {effectiveResponsesLimit && ( + + {" "} + / {effectiveResponsesLimit.toLocaleString()} + + )} +

+ {effectiveResponsesLimit && ( +
+
0.9 ? "bg-red-500" : "bg-teal-500" + )} + style={{ width: `${Math.min((responseCount / effectiveResponsesLimit) * 100, 100)}%` }} + />
)}
-
-
-

{t("common.responses")}

- {organization.billing.limits.monthly.responses && ( - +

Identified Contacts

+

+ {peopleCount.toLocaleString()} + {effectiveMiuLimit && ( + + {" "} + / {effectiveMiuLimit.toLocaleString()} + + )} +

+ {effectiveMiuLimit && ( +
+
0.9 ? "bg-red-500" : "bg-teal-500" + )} + style={{ width: `${Math.min((peopleCount / effectiveMiuLimit) * 100, 100)}%` }} /> - )} +
+ )} +
- {responsesUnlimitedCheck && ( - +

Workspaces

+

+ {projectCount} + {effectiveProjectsLimit && ( + / {effectiveProjectsLimit} + )} +

+ {effectiveProjectsLimit && ( +
+
0.9 ? "bg-red-500" : "bg-teal-500" + )} + style={{ width: `${Math.min((projectCount / effectiveProjectsLimit) * 100, 100)}%` }} /> - )} -
- -
-

- {t("environments.settings.billing.monthly_identified_users")} -

- {organization.billing.limits.monthly.miu && ( - - )} - - {peopleUnlimitedCheck && ( - - )} -
- -
-

{t("common.workspaces")}

- {organization.billing.limits.projects && ( - - )} - - {projectsUnlimitedCheck && ( - - )} -
+
+ )}
- {hasBillingRights && ( -
-
-
- - -
-
- +

Your volumes renew on Feb 1, 2025

+ + + {/* Overage Card (only for paid plans or trial) */} + {(isOnPaidPlan || effectiveTrialEndsAt) && ( + + { + if (demoPlan) { + toast.success(`Demo: Would change overage mode to ${mode}`); + setDemoOverageMode(mode); + return; + } + // TODO: Implement actual overage mode change via API + toast.success(`Overage mode changed to ${mode}`); + setDemoOverageMode(mode); + }} + onSpendingLimitChange={async (limit) => { + if (demoPlan) { + toast.success( + limit ? `Demo: Would set spending limit to $${limit}` : "Demo: Would remove spending limit" + ); + setDemoSpendingLimit(limit); + return; + } + // TODO: Implement actual spending limit change via API + toast.success(limit ? `Spending limit set to $${limit}` : "Spending limit removed"); + setDemoSpendingLimit(limit); + }} + /> + + )} + + {/* Alert: Annual Billing Upgrade (only for monthly paid plans) */} + {isOnMonthlyBilling && ( + + Save 20% on Annual Billing + Simplify your billing cycle and get 2 months free. + Switch to Annual + + )} + + {/* Alert: Special Pricing Programs (only for non-paid plans) */} + {!isOnPaidPlan && ( + + Special Pricing Programs + + Exclusive discounts for Startups, Non-profits, and Open Source projects. + + { + if (demoPlan) { + toast.success("Demo: Would open discount application form"); + return; + } + globalThis.open("https://formbricks.com/discount", "_blank"); + }}> + Apply for Discount + + + )} + + {/* Pricing Plans */} + {hasBillingRights && ( +
+ {/* Period Toggle */} +
+
+ +
+ {planPeriod === "yearly" && ( + + Get 2 months free 🔥 + + )}
- )} -
-
+ + {/* Plan Cards */} +
+ {getCloudPricingData(t).plans.map((plan) => ( + { + await onUpgrade(plan.id); + }} + organization={effectiveOrganization} + onManageSubscription={openCustomerPortal} + isTrialActive={demoPlan === "trial" && plan.id === "scale"} + currentPlan={effectivePlan} + /> + ))} +
+
+ )} +
); }; diff --git a/apps/web/modules/ee/billing/components/select-plan-card.tsx b/apps/web/modules/ee/billing/components/select-plan-card.tsx new file mode 100644 index 0000000000..07698f83cf --- /dev/null +++ b/apps/web/modules/ee/billing/components/select-plan-card.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { CheckIcon, GiftIcon } from "lucide-react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import calLogo from "@/images/customer-logos/cal-logo-light.svg"; +import ethereumLogo from "@/images/customer-logos/ethereum-logo.png"; +import flixbusLogo from "@/images/customer-logos/flixbus-white.svg"; +import githubLogo from "@/images/customer-logos/github-logo.png"; +import siemensLogo from "@/images/customer-logos/siemens.png"; +import { Button } from "@/modules/ui/components/button"; + +interface SelectPlanCardProps { + /** URL to redirect after starting trial or continuing with free */ + nextUrl: string; +} + +const TRIAL_FEATURES = [ + "Fully white-labeled surveys", + "All team & collaboration features", + "Setup custom webhooks", + "Get full API access", + "Setup email follow-ups", + "Manage quotas", +]; + +const CUSTOMER_LOGOS = [ + { src: siemensLogo, alt: "Siemens" }, + { src: calLogo, alt: "Cal.com" }, + { src: flixbusLogo, alt: "FlixBus" }, + { src: githubLogo, alt: "GitHub" }, + { src: ethereumLogo, alt: "Ethereum" }, +]; + +export const SelectPlanCard = ({ nextUrl }: SelectPlanCardProps) => { + const router = useRouter(); + const [isStartingTrial, setIsStartingTrial] = useState(false); + + const handleStartTrial = async () => { + setIsStartingTrial(true); + // TODO: Implement trial activation via Stripe + router.push(nextUrl); + }; + + const handleContinueFree = () => { + router.push(nextUrl); + }; + + return ( +
+ {/* Trial Card */} +
+
+ {/* Gift Icon */} +
+ +
+ + {/* Title & Subtitle */} +
+

Try Pro features for free!

+

14 days trial, no credit card required

+
+ + {/* Features List */} +
    + {TRIAL_FEATURES.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + {/* CTA Button */} + +
+ + {/* Logo Carousel */} +
+
+ {/* Duplicate logos for seamless infinite scroll */} + {[...CUSTOMER_LOGOS, ...CUSTOMER_LOGOS].map((logo, index) => ( +
+ {logo.alt} +
+ ))} +
+
+
+ + {/* Skip Option */} + +
+ ); +}; diff --git a/apps/web/modules/ee/billing/components/settings-id.tsx b/apps/web/modules/ee/billing/components/settings-id.tsx new file mode 100644 index 0000000000..a65c113f41 --- /dev/null +++ b/apps/web/modules/ee/billing/components/settings-id.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { HelpCircleIcon } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; + +interface SettingsIdProps { + label: string; + value: string; + tooltip?: string; + className?: string; + align?: "left" | "right"; +} + +export const SettingsId = ({ label, value, tooltip, className, align = "left" }: SettingsIdProps) => { + return ( +
+
+ {label} + {tooltip && ( + + + + + + +

{tooltip}

+
+
+
+ )} +
+ {value} +
+ ); +}; diff --git a/apps/web/modules/ee/billing/page.tsx b/apps/web/modules/ee/billing/page.tsx index b8ba82b4ce..1726483e26 100644 --- a/apps/web/modules/ee/billing/page.tsx +++ b/apps/web/modules/ee/billing/page.tsx @@ -1,7 +1,6 @@ import { notFound } from "next/navigation"; import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; -import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; -import { PROJECT_FEATURE_KEYS, STRIPE_PRICE_LOOKUP_KEYS } from "@/lib/constants"; +import { IS_FORMBRICKS_CLOUD, STRIPE_PRICE_LOOKUP_KEYS } from "@/lib/constants"; import { getMonthlyActiveOrganizationPeopleCount, getMonthlyOrganizationResponseCount, @@ -49,7 +48,6 @@ export const PricingPage = async (props) => { responseCount={responseCount} projectCount={projectCount} stripePriceLookupKeys={STRIPE_PRICE_LOOKUP_KEYS} - projectFeatureKeys={PROJECT_FEATURE_KEYS} hasBillingRights={hasBillingRights} /> diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 120265f69d..4a632d8d46 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -20,6 +20,7 @@ module.exports = { fadeOut: "fadeOut 0.2s ease-out", surveyLoading: "surveyLoadingAnimation 0.5s ease-out forwards", surveyExit: "surveyExitAnimation 0.5s ease-out forwards", + "logo-scroll": "logo-scroll 20s linear infinite", }, blur: { xxs: "0.33px", @@ -130,6 +131,10 @@ module.exports = { "0%": { transform: "translateY(0)", opacity: "1" }, "100%": { transform: "translateY(-50px)", opacity: "0" }, }, + "logo-scroll": { + "0%": { transform: "translateX(0)" }, + "100%": { transform: "translateX(-50%)" }, + }, }, width: { "sidebar-expanded": "4rem",