diff --git a/plugins/CLAUDE.md b/plugins/CLAUDE.md
new file mode 100644
index 0000000..adfdcb1
--- /dev/null
+++ b/plugins/CLAUDE.md
@@ -0,0 +1,7 @@
+
+# Recent Activity
+
+
+
+*No recent activity*
+
\ No newline at end of file
diff --git a/plugins/withStoreKitConfig.js b/plugins/withStoreKitConfig.js
new file mode 100644
index 0000000..11fb6a9
--- /dev/null
+++ b/plugins/withStoreKitConfig.js
@@ -0,0 +1,148 @@
+/**
+ * Expo Config Plugin: StoreKit Configuration for Sandbox Testing
+ *
+ * Adds TabataFit.storekit to the Xcode project and configures the scheme
+ * to use it for StoreKit testing. This enables purchase testing in the
+ * iOS simulator without real Apple Pay charges.
+ */
+
+const {
+ withXcodeProject,
+ withDangerousMod,
+} = require('@expo/config-plugins')
+const fs = require('fs')
+const path = require('path')
+
+const STOREKIT_FILENAME = 'TabataFit.storekit'
+
+/**
+ * Step 1: Copy the .storekit file and add it to the Xcode project (project.pbxproj)
+ */
+function withStoreKitXcodeProject(config) {
+ return withXcodeProject(config, (config) => {
+ const project = config.modResults
+ const projectName = config.modRequest.projectName
+ const platformRoot = config.modRequest.platformProjectRoot
+
+ // Copy .storekit file into the iOS app directory
+ const sourceFile = path.resolve(__dirname, '..', 'storekit', STOREKIT_FILENAME)
+ const destDir = path.join(platformRoot, projectName)
+ const destFile = path.join(destDir, STOREKIT_FILENAME)
+ fs.copyFileSync(sourceFile, destFile)
+
+ // Add file to the Xcode project manually via pbxproj APIs
+ // Find the app's PBXGroup
+ const mainGroupKey = project.getFirstProject().firstProject.mainGroup
+ const mainGroup = project.getPBXGroupByKey(mainGroupKey)
+
+ // Find the app target group (e.g., "tabatago")
+ let appGroupKey = null
+ if (mainGroup && mainGroup.children) {
+ for (const child of mainGroup.children) {
+ if (child.comment === projectName) {
+ appGroupKey = child.value
+ break
+ }
+ }
+ }
+
+ if (!appGroupKey) {
+ console.warn('[withStoreKitConfig] Could not find app group in Xcode project')
+ return config
+ }
+
+ const appGroup = project.getPBXGroupByKey(appGroupKey)
+
+ // Check if already added
+ const alreadyExists = appGroup.children?.some(
+ (child) => child.comment === STOREKIT_FILENAME
+ )
+
+ if (!alreadyExists) {
+ // Generate a unique UUID for the file reference
+ const fileRefUuid = project.generateUuid()
+
+ // Add PBXFileReference — NOT added to any build phase
+ // .storekit files are testing configs, not app resources
+ const pbxFileRef = project.hash.project.objects['PBXFileReference']
+ pbxFileRef[fileRefUuid] = {
+ isa: 'PBXFileReference',
+ lastKnownFileType: 'text.json',
+ path: STOREKIT_FILENAME,
+ sourceTree: '""',
+ }
+ pbxFileRef[`${fileRefUuid}_comment`] = STOREKIT_FILENAME
+
+ // Add to the app group's children (visible in Xcode navigator)
+ appGroup.children.push({
+ value: fileRefUuid,
+ comment: STOREKIT_FILENAME,
+ })
+
+ console.log('[withStoreKitConfig] Added', STOREKIT_FILENAME, 'to Xcode project')
+ }
+
+ return config
+ })
+}
+
+/**
+ * Step 2: Configure the Xcode scheme to use the StoreKit configuration
+ */
+function withStoreKitScheme(config) {
+ return withDangerousMod(config, [
+ 'ios',
+ (config) => {
+ const projectName = config.modRequest.projectName
+ const schemePath = path.join(
+ config.modRequest.platformProjectRoot,
+ `${projectName}.xcodeproj`,
+ 'xcshareddata',
+ 'xcschemes',
+ `${projectName}.xcscheme`
+ )
+
+ if (!fs.existsSync(schemePath)) {
+ console.warn('[withStoreKitConfig] Scheme not found:', schemePath)
+ return config
+ }
+
+ let scheme = fs.readFileSync(schemePath, 'utf8')
+
+ // Skip if already configured
+ if (scheme.includes('storeKitConfigurationFileReference')) {
+ console.log('[withStoreKitConfig] StoreKit config already in scheme')
+ return config
+ }
+
+ // Insert StoreKitConfigurationFileReference as a child element of LaunchAction
+ // The identifier path is relative to the workspace root (ios/ directory)
+ const storeKitRef = `
+
+ `
+
+ // Insert before the closing tag
+ scheme = scheme.replace(
+ '',
+ `${storeKitRef}\n `
+ )
+
+ fs.writeFileSync(schemePath, scheme, 'utf8')
+ console.log('[withStoreKitConfig] Added StoreKit config to scheme')
+
+ return config
+ },
+ ])
+}
+
+/**
+ * Main plugin
+ */
+function withStoreKitConfig(config) {
+ config = withStoreKitXcodeProject(config)
+ config = withStoreKitScheme(config)
+ return config
+}
+
+module.exports = withStoreKitConfig
diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts
index a7e7e08..2070ad6 100644
--- a/src/shared/hooks/index.ts
+++ b/src/shared/hooks/index.ts
@@ -6,3 +6,5 @@ export { useTimer } from './useTimer'
export type { TimerPhase } from './useTimer'
export { useHaptics } from './useHaptics'
export { useAudio } from './useAudio'
+export { useNotifications, requestNotificationPermissions } from './useNotifications'
+export { usePurchases } from './usePurchases'
diff --git a/src/shared/hooks/useNotifications.ts b/src/shared/hooks/useNotifications.ts
new file mode 100644
index 0000000..4b0d03d
--- /dev/null
+++ b/src/shared/hooks/useNotifications.ts
@@ -0,0 +1,56 @@
+/**
+ * TabataFit Notification Hook
+ * Manages daily reminder scheduling via expo-notifications
+ */
+
+import { useEffect } from 'react'
+import * as Notifications from 'expo-notifications'
+import { useUserStore } from '@/src/shared/stores'
+import i18n from '@/src/shared/i18n'
+
+const REMINDER_ID = 'daily-reminder'
+
+export async function requestNotificationPermissions(): Promise {
+ const { status: existing } = await Notifications.getPermissionsAsync()
+ if (existing === 'granted') return true
+
+ const { status } = await Notifications.requestPermissionsAsync()
+ return status === 'granted'
+}
+
+async function scheduleDaily(time: string) {
+ await Notifications.cancelAllScheduledNotificationsAsync()
+
+ const [hour, minute] = time.split(':').map(Number)
+
+ await Notifications.scheduleNotificationAsync({
+ identifier: REMINDER_ID,
+ content: {
+ title: i18n.t('notifications:dailyReminder.title'),
+ body: i18n.t('notifications:dailyReminder.body'),
+ sound: true,
+ },
+ trigger: {
+ type: Notifications.SchedulableTriggerInputTypes.DAILY,
+ hour,
+ minute,
+ },
+ })
+}
+
+async function cancelAll() {
+ await Notifications.cancelAllScheduledNotificationsAsync()
+}
+
+export function useNotifications() {
+ const reminders = useUserStore((s) => s.settings.reminders)
+ const reminderTime = useUserStore((s) => s.settings.reminderTime)
+
+ useEffect(() => {
+ if (reminders) {
+ scheduleDaily(reminderTime)
+ } else {
+ cancelAll()
+ }
+ }, [reminders, reminderTime])
+}
diff --git a/src/shared/hooks/usePurchases.ts b/src/shared/hooks/usePurchases.ts
new file mode 100644
index 0000000..379ffc1
--- /dev/null
+++ b/src/shared/hooks/usePurchases.ts
@@ -0,0 +1,257 @@
+/**
+ * TabataFit Purchases Hook
+ * Wraps RevenueCat API for subscription management
+ *
+ * DEV mode: If StoreKit purchase fails (no sandbox account / product mismatch),
+ * shows an Alert offering to simulate the purchase for testing.
+ */
+
+import { useState, useEffect, useCallback } from 'react'
+import { Alert } from 'react-native'
+import Purchases, {
+ CustomerInfo,
+ PurchasesOfferings,
+ PurchasesPackage,
+} from 'react-native-purchases'
+
+import { useUserStore } from '../stores'
+import { ENTITLEMENT_ID } from '../services/purchases'
+import type { SubscriptionPlan } from '../types'
+
+interface PurchaseResult {
+ success: boolean
+ cancelled: boolean
+ error?: string
+}
+
+interface UsePurchasesReturn {
+ isPremium: boolean
+ isLoading: boolean
+ monthlyPackage: PurchasesPackage | null
+ annualPackage: PurchasesPackage | null
+ purchasePackage: (pkg: PurchasesPackage) => Promise
+ restorePurchases: () => Promise
+}
+
+/**
+ * Helper to check if user has premium entitlement
+ */
+function hasPremiumEntitlement(info: CustomerInfo | null): boolean {
+ if (!info) return false
+ return ENTITLEMENT_ID in info.entitlements.active
+}
+
+/**
+ * Hook to manage RevenueCat subscriptions
+ * Syncs subscription state with userStore
+ */
+export function usePurchases(): UsePurchasesReturn {
+ const [isLoading, setIsLoading] = useState(true)
+ const [offerings, setOfferings] = useState(null)
+ const [customerInfo, setCustomerInfo] = useState(null)
+
+ const subscription = useUserStore((s) => s.profile.subscription)
+ const setSubscription = useUserStore((s) => s.setSubscription)
+
+ // Derive premium status from RevenueCat entitlement or local state
+ const isPremium = customerInfo
+ ? hasPremiumEntitlement(customerInfo)
+ : subscription !== 'free'
+
+ // Get packages from offerings
+ const monthlyPackage = offerings?.current?.monthly ?? null
+ const annualPackage = offerings?.current?.annual ?? null
+
+ // Sync RevenueCat state to userStore
+ const syncSubscriptionToStore = useCallback(
+ (info: CustomerInfo | null) => {
+ if (!info) return
+
+ const hasPremium = hasPremiumEntitlement(info)
+ const activeSubscriptions = info.activeSubscriptions
+
+ let newPlan: SubscriptionPlan = 'free'
+
+ if (hasPremium && activeSubscriptions.length > 0) {
+ // Determine plan type from subscription identifier
+ const subId = activeSubscriptions[0].toLowerCase()
+ if (subId.includes('yearly') || subId.includes('annual')) {
+ newPlan = 'premium-yearly'
+ } else if (subId.includes('monthly')) {
+ newPlan = 'premium-monthly'
+ } else {
+ // Default to yearly for any premium entitlement
+ newPlan = 'premium-yearly'
+ }
+ }
+
+ // Only update if different
+ if (subscription !== newPlan) {
+ console.log('[Purchases] Syncing subscription to store:', newPlan)
+ setSubscription(newPlan)
+ }
+ },
+ [subscription, setSubscription]
+ )
+
+ // Fetch offerings and customer info on mount
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [offeringsResult, customerInfoResult] = await Promise.all([
+ Purchases.getOfferings(),
+ Purchases.getCustomerInfo(),
+ ])
+
+ setOfferings(offeringsResult)
+ setCustomerInfo(customerInfoResult)
+ syncSubscriptionToStore(customerInfoResult)
+
+ console.log('[Purchases] Offerings loaded:', {
+ hasMonthly: !!offeringsResult.current?.monthly,
+ hasAnnual: !!offeringsResult.current?.annual,
+ monthlyProductId: offeringsResult.current?.monthly?.product.identifier,
+ annualProductId: offeringsResult.current?.annual?.product.identifier,
+ monthlyPrice: offeringsResult.current?.monthly?.product.priceString,
+ annualPrice: offeringsResult.current?.annual?.product.priceString,
+ })
+ } catch (error) {
+ console.error('[Purchases] Failed to fetch offerings/customerInfo:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchData()
+ }, [syncSubscriptionToStore])
+
+ // Listen for customer info changes (renewals, expirations, etc.)
+ useEffect(() => {
+ const listener = (info: CustomerInfo) => {
+ console.log('[Purchases] Customer info updated')
+ setCustomerInfo(info)
+ syncSubscriptionToStore(info)
+ }
+
+ Purchases.addCustomerInfoUpdateListener(listener)
+
+ return () => {
+ Purchases.removeCustomerInfoUpdateListener(listener)
+ }
+ }, [syncSubscriptionToStore])
+
+ // Purchase a package
+ const purchasePackage = useCallback(
+ async (pkg: PurchasesPackage): Promise => {
+ try {
+ console.log('[Purchases] Starting purchase for:', pkg.identifier, pkg.product.identifier)
+ const { customerInfo: newInfo } = await Purchases.purchasePackage(pkg)
+
+ setCustomerInfo(newInfo)
+ syncSubscriptionToStore(newInfo)
+
+ const success = hasPremiumEntitlement(newInfo)
+ console.log('[Purchases] Purchase result:', { success })
+
+ return { success, cancelled: false }
+ } catch (error: any) {
+ // Handle user cancellation
+ if (error.userCancelled) {
+ console.log('[Purchases] Purchase cancelled by user')
+ return { success: false, cancelled: true }
+ }
+
+ console.error('[Purchases] Purchase error:', error)
+
+ // DEV mode: offer to simulate the purchase when StoreKit fails
+ if (__DEV__) {
+ return new Promise((resolve) => {
+ Alert.alert(
+ 'Purchase failed (DEV)',
+ `StoreKit error: ${error.message || 'Unknown error'}\n\nProduct: ${pkg.product.identifier}\n\nSimulate a successful purchase for testing?`,
+ [
+ {
+ text: 'Cancel',
+ style: 'cancel',
+ onPress: () =>
+ resolve({ success: false, cancelled: true }),
+ },
+ {
+ text: 'Simulate Purchase',
+ onPress: () => {
+ // Determine plan from package identifier
+ const id = pkg.product.identifier.toLowerCase()
+ const plan: SubscriptionPlan =
+ id.includes('annual') || id.includes('year')
+ ? 'premium-yearly'
+ : 'premium-monthly'
+ setSubscription(plan)
+ console.log('[Purchases] DEV: Simulated purchase →', plan)
+ resolve({ success: true, cancelled: false })
+ },
+ },
+ ]
+ )
+ })
+ }
+
+ return {
+ success: false,
+ cancelled: false,
+ error: error.message || 'Purchase failed',
+ }
+ }
+ },
+ [syncSubscriptionToStore, setSubscription]
+ )
+
+ // Restore purchases
+ const restorePurchases = useCallback(async (): Promise => {
+ try {
+ console.log('[Purchases] Restoring purchases...')
+ const restoredInfo = await Purchases.restorePurchases()
+
+ setCustomerInfo(restoredInfo)
+ syncSubscriptionToStore(restoredInfo)
+
+ const hasPremium = hasPremiumEntitlement(restoredInfo)
+ console.log('[Purchases] Restore result:', { hasPremium })
+
+ return hasPremium
+ } catch (error) {
+ console.error('[Purchases] Restore failed:', error)
+
+ // DEV mode: offer to simulate restore
+ if (__DEV__) {
+ return new Promise((resolve) => {
+ Alert.alert(
+ 'Restore failed (DEV)',
+ 'Simulate a restored premium subscription?',
+ [
+ { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
+ {
+ text: 'Simulate Restore',
+ onPress: () => {
+ setSubscription('premium-yearly')
+ console.log('[Purchases] DEV: Simulated restore → premium-yearly')
+ resolve(true)
+ },
+ },
+ ]
+ )
+ })
+ }
+
+ return false
+ }
+ }, [syncSubscriptionToStore, setSubscription])
+
+ return {
+ isPremium,
+ isLoading,
+ monthlyPackage,
+ annualPackage,
+ purchasePackage,
+ restorePurchases,
+ }
+}
diff --git a/src/shared/services/CLAUDE.md b/src/shared/services/CLAUDE.md
new file mode 100644
index 0000000..081c948
--- /dev/null
+++ b/src/shared/services/CLAUDE.md
@@ -0,0 +1,16 @@
+
+# Recent Activity
+
+
+
+### Feb 20, 2026
+
+| ID | Time | T | Title | Read |
+|----|------|---|-------|------|
+| #5392 | 10:22 PM | 🔵 | User requests sandbox purchase testing capability for RevenueCat | ~514 |
+| #5391 | 10:20 PM | 🟣 | RevenueCat subscription system fully integrated and tested | ~656 |
+| #5386 | 10:16 PM | ✅ | Updated RevenueCat API key in production configuration | ~115 |
+| #5384 | 9:28 PM | 🟣 | Implemented RevenueCat subscription system | ~190 |
+| #5353 | 7:47 PM | 🟣 | RevenueCat Service Initialization Module Created | ~244 |
+| #5275 | 2:42 PM | 🟣 | Implemented daily reminder feature for TabataGo app | ~204 |
+
\ No newline at end of file
diff --git a/src/shared/services/analytics.ts b/src/shared/services/analytics.ts
new file mode 100644
index 0000000..55909a4
--- /dev/null
+++ b/src/shared/services/analytics.ts
@@ -0,0 +1,84 @@
+/**
+ * TabataFit PostHog Analytics Service
+ * Initialize and configure PostHog for user analytics
+ *
+ * Follows the same pattern as purchases.ts:
+ * - initializeAnalytics() called once at app startup
+ * - track() helper for type-safe event tracking
+ * - identifyUser() for user identification
+ */
+
+import PostHog from 'posthog-react-native'
+
+type EventProperties = Record
+
+// PostHog configuration
+// Replace with your actual PostHog project API key
+const POSTHOG_API_KEY = '__YOUR_POSTHOG_API_KEY__'
+const POSTHOG_HOST = 'https://us.i.posthog.com'
+
+// Singleton client instance
+let posthogClient: PostHog | null = null
+
+/**
+ * Initialize PostHog SDK
+ * Call this once at app startup (after store hydration)
+ */
+export async function initializeAnalytics(): Promise {
+ if (posthogClient) {
+ console.log('[Analytics] Already initialized')
+ return posthogClient
+ }
+
+ // Skip initialization if no real API key is configured
+ if (POSTHOG_API_KEY.startsWith('__')) {
+ if (__DEV__) {
+ console.log('[Analytics] No API key configured — events will be logged to console only')
+ }
+ return null
+ }
+
+ try {
+ posthogClient = new PostHog(POSTHOG_API_KEY, {
+ host: POSTHOG_HOST,
+ enableSessionReplay: false,
+ })
+
+ console.log('[Analytics] PostHog initialized successfully')
+ return posthogClient
+ } catch (error) {
+ console.error('[Analytics] Failed to initialize PostHog:', error)
+ return null
+ }
+}
+
+/**
+ * Get the PostHog client instance (for PostHogProvider)
+ */
+export function getPostHogClient(): PostHog | null {
+ return posthogClient
+}
+
+/**
+ * Track an analytics event
+ * Safe to call even if PostHog is not initialized — logs to console in dev
+ */
+export function track(event: string, properties?: EventProperties): void {
+ if (__DEV__) {
+ console.log(`[Analytics] ${event}`, properties ?? '')
+ }
+ posthogClient?.capture(event, properties)
+}
+
+/**
+ * Identify a user with traits
+ */
+export function identifyUser(
+ userId: string,
+ traits?: EventProperties,
+): void {
+ if (__DEV__) {
+ console.log('[Analytics] identify', userId, traits ?? '')
+ }
+ posthogClient?.identify(userId, traits)
+}
diff --git a/src/shared/services/purchases.ts b/src/shared/services/purchases.ts
new file mode 100644
index 0000000..e3ca0a5
--- /dev/null
+++ b/src/shared/services/purchases.ts
@@ -0,0 +1,56 @@
+/**
+ * TabataFit RevenueCat Service
+ * Initialize and configure RevenueCat for Apple subscriptions
+ *
+ * Sandbox testing:
+ * - The test_ API key enables RevenueCat sandbox mode
+ * - StoreKit Configuration (TabataFit.storekit) enables simulator purchases
+ * - Transactions are free sandbox completions — no real charges
+ */
+
+import Purchases, { LOG_LEVEL } from 'react-native-purchases'
+
+// RevenueCat configuration
+// test_ prefix = sandbox mode (free transactions on simulator + device sandbox accounts)
+export const REVENUECAT_API_KEY = 'test_oIJbIHWISJaUZdgxRMHlwizBHvM'
+
+// Entitlement ID configured in RevenueCat dashboard
+export const ENTITLEMENT_ID = 'premium'
+
+// Track initialization state
+let isInitialized = false
+
+/**
+ * Initialize RevenueCat SDK
+ * Call this once at app startup (after store hydration)
+ */
+export async function initializePurchases(): Promise {
+ if (isInitialized) {
+ console.log('[Purchases] Already initialized')
+ return
+ }
+
+ try {
+ // setLogLevel MUST be called before configure()
+ if (__DEV__) {
+ Purchases.setLogLevel(LOG_LEVEL.VERBOSE)
+ }
+
+ // Configure RevenueCat with API key
+ await Purchases.configure({ apiKey: REVENUECAT_API_KEY })
+
+ isInitialized = true
+ console.log('[Purchases] RevenueCat initialized successfully')
+ console.log('[Purchases] Sandbox mode:', __DEV__ ? 'enabled' : 'disabled')
+ } catch (error) {
+ console.error('[Purchases] Failed to initialize RevenueCat:', error)
+ throw error
+ }
+}
+
+/**
+ * Check if RevenueCat has been initialized
+ */
+export function isPurchasesInitialized(): boolean {
+ return isInitialized
+}
diff --git a/storekit/CLAUDE.md b/storekit/CLAUDE.md
new file mode 100644
index 0000000..adfdcb1
--- /dev/null
+++ b/storekit/CLAUDE.md
@@ -0,0 +1,7 @@
+
+# Recent Activity
+
+
+
+*No recent activity*
+
\ No newline at end of file
diff --git a/storekit/TabataFit.storekit b/storekit/TabataFit.storekit
new file mode 100644
index 0000000..f6e8e9b
--- /dev/null
+++ b/storekit/TabataFit.storekit
@@ -0,0 +1,126 @@
+{
+ "identifier" : "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
+ "type" : "Default",
+ "version" : {
+ "major" : 4,
+ "minor" : 0
+ },
+ "settings" : {
+ "_applicationInternalID" : "",
+ "_developerTeamID" : "",
+ "_failTransactionsEnabled" : false,
+ "_lastSynchronizedDate" : null,
+ "_locale" : "en_US",
+ "_storefront" : "USA",
+ "_storeKitErrors" : [
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Load Products"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Purchase"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Verification"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "App Store Sync"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Subscription Status"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "App Transaction"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Manage Subscriptions Sheet"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Refund Request Sheet"
+ },
+ {
+ "current" : null,
+ "enabled" : false,
+ "name" : "Offer Code Redeem Sheet"
+ }
+ ]
+ },
+ "subscriptionGroups" : [
+ {
+ "id" : "F1A2B3C4-D5E6-7890-ABCD-111111111111",
+ "localizations" : [],
+ "name" : "TabataFit Premium",
+ "subscriptions" : [
+ {
+ "adHocOffers" : [],
+ "codeOffers" : [],
+ "displayPrice" : "6.99",
+ "familyShareable" : true,
+ "groupNumber" : 1,
+ "internalID" : "A1111111-1111-1111-1111-111111111111",
+ "introductoryOffer" : {
+ "displayPrice" : "0",
+ "internalID" : "B1111111-1111-1111-1111-111111111111",
+ "numberOfPeriods" : 1,
+ "paymentMode" : "free",
+ "subscriptionPeriod" : "P1W"
+ },
+ "localizations" : [
+ {
+ "description" : "Monthly access to all TabataFit workouts and features",
+ "displayName" : "TabataFit+ Monthly",
+ "locale" : "en_US"
+ }
+ ],
+ "productID" : "tabatafit_premium_monthly",
+ "recurringSubscriptionPeriod" : "P1M",
+ "referenceName" : "Monthly Premium",
+ "subscriptionGroupID" : "F1A2B3C4-D5E6-7890-ABCD-111111111111",
+ "type" : "RecurringSubscription"
+ },
+ {
+ "adHocOffers" : [],
+ "codeOffers" : [],
+ "displayPrice" : "49.99",
+ "familyShareable" : true,
+ "groupNumber" : 1,
+ "internalID" : "A2222222-2222-2222-2222-222222222222",
+ "introductoryOffer" : {
+ "displayPrice" : "0",
+ "internalID" : "B2222222-2222-2222-2222-222222222222",
+ "numberOfPeriods" : 1,
+ "paymentMode" : "free",
+ "subscriptionPeriod" : "P1W"
+ },
+ "localizations" : [
+ {
+ "description" : "Annual access to all TabataFit workouts — save 40%",
+ "displayName" : "TabataFit+ Annual",
+ "locale" : "en_US"
+ }
+ ],
+ "productID" : "tabatafit_premium_annual",
+ "recurringSubscriptionPeriod" : "P1Y",
+ "referenceName" : "Annual Premium",
+ "subscriptionGroupID" : "F1A2B3C4-D5E6-7890-ABCD-111111111111",
+ "type" : "RecurringSubscription"
+ }
+ ]
+ }
+ ]
+}