0Pricing
React Native Academy · Lesson

Polishing, Testing, and Shipping to Both Stores

Write unit tests for core hooks, add a Maestro E2E flow for the critical path, optimize the bundle, generate store assets, and submit to both the App Store and Google Play.

Polishing, Testing, and Shipping to Both Stores is a free React Native Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Native Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Final Stretch: Polish, Test, Ship

The last phase of any app project is the most discipline-intensive: polishing the rough edges users will actually notice, writing tests that catch regressions before they reach production, and executing the submission process for both stores. Each of these feels like it should be quick but takes as long as the feature work — budget time accordingly. A well-polished app with proper test coverage ships with confidence.

UI Polish: Consistent Spacing and Typography

Polish starts with design tokens — a single source of truth for spacing, font sizes, font weights, border radii, and colors. Define these in a theme.ts file and import them everywhere instead of hard-coding padding: 16 scattered across screens. Consistency is what makes an app feel premium — when all buttons have the same height, all headings use the same font, and all cards have the same shadow, the UI feels intentional.

// src/theme.ts
export const theme = {
  colors: {
    primary: '#007AFF',
    background: '#F2F2F7',
    surface: '#FFFFFF',
    text: '#000000',
    textSecondary: '#8E8E93',
    error: '#FF3B30',
    success: '#34C759',
  },
  spacing: {
    xs: 4, sm: 8, md: 16, lg: 24, xl: 32
  },
  typography: {
    largeTitle: { fontSize: 34, fontWeight: '700' },
    title: { fontSize: 22, fontWeight: '600' },
    body: { fontSize: 17, fontWeight: '400' },
    caption: { fontSize: 12, fontWeight: '400' },
  },
  borderRadius: { sm: 8, md: 12, lg: 16, full: 999 },
};

Accessibility Audit

Accessibility is both a legal requirement and a quality marker. Audit your app for: accessible labels on all icon buttons (accessibilityLabel), minimum touch target sizes of 44×44pt on iOS and 48×48dp on Android, correct accessibility roles (accessibilityRole='button' for tappable items), and color contrast (at least 4.5:1 for body text). Use the iOS Accessibility Inspector or Android TalkBack to test screen reader behavior.

// Accessible icon button
<TouchableOpacity
  onPress={handleDelete}
  accessibilityLabel='Delete habit'
  accessibilityRole='button'
  accessibilityHint='Permanently removes this habit and its history'
  style={{ padding: 12 }}  // Ensures 44pt touch target with 20pt icon
>
  <Ionicons name='trash-outline' size={20} color='#FF3B30' />
</TouchableOpacity>

// Screen reader text for images
<Image
  source={{ uri: habit.icon }}
  accessibilityLabel={`${habit.name} habit icon`}
  accessible={true}
/>

Writing Unit Tests for Core Hooks

Unit-test the business logic that is independent of the UI — calculations, hooks with mocked queries, and utility functions. The streak calculation is a perfect candidate: test edge cases like an empty array, a single day, a gap in the middle, and today not completed. Use @testing-library/react-native with renderHook to test custom hooks that use React state and effects.

// src/utils/__tests__/streaks.test.ts
import { calculateStreak } from '../streaks';

describe('calculateStreak', () => {
  it('returns 0 for empty completions', () => {
    expect(calculateStreak([])).toBe(0);
  });

  it('returns 1 when only today is completed', () => {
    const today = new Date().toISOString().split('T')[0];
    expect(calculateStreak([today])).toBe(1);
  });

  it('returns correct streak for consecutive days', () => {
    const dates = ['2026-06-21', '2026-06-20', '2026-06-19'];
    // Mock today as 2026-06-21 in tests
    expect(calculateStreak(dates)).toBe(3);
  });

  it('stops at a gap', () => {
    // Today=21, gap at 20, then 19
    const dates = ['2026-06-21', '2026-06-19'];
    expect(calculateStreak(dates)).toBe(1);
  });
});

Component Tests with React Native Testing Library

Write component tests for the most-used UI elements: the HabitCard rendering habit name and streak, the CheckInButton toggling state on press, and the OfflineBanner appearing only when offline. Mock dependencies (Supabase, React Query) at the test boundary so components are tested in isolation without real network calls.

// src/components/__tests__/HabitCard.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { HabitCard } from '../HabitCard';

const mockHabit = {
  id: '1', name: 'Morning Run', icon: 'run',
  color: '#007AFF', streak: 5
};

it('renders habit name and streak', () => {
  const { getByText } = render(<HabitCard habit={mockHabit} onPress={() => {}} />);
  expect(getByText('Morning Run')).toBeTruthy();
  expect(getByText('5 days')).toBeTruthy();
});

it('calls onPress when tapped', () => {
  const onPress = jest.fn();
  const { getByTestId } = render(
    <HabitCard habit={mockHabit} onPress={onPress} testID='habit-card' />
  );
  fireEvent.press(getByTestId('habit-card'));
  expect(onPress).toHaveBeenCalledTimes(1);
});

Maestro E2E: Critical Path Test

Write a Maestro flow for the critical user path: sign in, create a habit, check it in, verify the streak updates, and sign out. This catches integration bugs that unit tests miss — navigation state, network calls, and UI state all working together. Run it against a real simulator or device on every CI build before promoting to the next test track.

# maestro/critical-path.yaml
---
appId: com.yourcompany.habittracker
---
- launchApp:
    clearState: true
- tapOn: 'Sign In'
- inputText: 'test@example.com'
- tapOn: 'Password'
- inputText: 'TestPass123!'
- tapOn: 'Sign In'
- waitForAnimationToEnd
- assertVisible: 'My Habits'
- tapOn: 'Add Habit'
- inputText: 'Morning Run'
- tapOn: 'Save'
- waitForAnimationToEnd
- assertVisible: 'Morning Run'
- tapOn:
    id: 'check-in-btn-1'
- waitForAnimationToEnd
- assertVisible: '1 day'  # Streak shows 1 day
- tapOn: 'Settings'
- tapOn: 'Sign Out'
- assertVisible: 'Sign In'

Bundle Size Optimization

Before submitting, analyze and minimize the JS bundle. Use react-native-bundle-visualizer to identify large dependencies. Common savings: replace moment.js (280KB) with date-fns (tree-shakeable, ~5KB per import), remove unused icon packs (only import icons you use), and apply lazy loading (React.lazy + Suspense) for heavy screens like settings or onboarding that users rarely visit after the first launch.

# Analyze bundle
npx react-native-bundle-visualizer

# Opens a visual treemap in browser showing:
# - node_modules breakdown by size
# - Your source code size
# - Duplicate modules

# Common large packages to replace:
# moment (280KB) -> date-fns tree-shaking
# lodash (full, 70KB) -> lodash-es specific imports
# FontAwesome all icons -> import only used icons

# Result: each 100KB reduction = ~50KB gzip reduction
# = faster first install + lower Play/App Store size

Generating Store Assets

Create the required store assets before submission: app icon (1024×1024 for iOS, adaptive icon for Android), splash screen (all size variants via Expo's splash config), screenshots (at least 3–6 per required device size), and a feature graphic (1024×500 for Android). Use a design tool like Figma with device frame plugins to create professional-looking screenshots without needing a marketing designer.

// app.json — icon and splash configuration
{
  'expo': {
    'icon': './assets/icon.png',        // 1024x1024 PNG
    'splash': {
      'image': './assets/splash.png',
      'resizeMode': 'contain',
      'backgroundColor': '#007AFF'
    },
    'ios': {
      'icon': './assets/icon.png'
    },
    'android': {
      'icon': './assets/icon.png',
      'adaptiveIcon': {
        'foregroundImage': './assets/adaptive-icon.png',
        'backgroundColor': '#007AFF'
      }
    }
  }
}

Final Build for Production

Trigger the production EAS builds for both platforms. Verify the version number and version code are correct before building — changing them after requires a new build. Run both builds in parallel to save time. Once both are complete, upload to TestFlight and Play Console internal testing for one final smoke test on the exact production binary before submitting for review.

# Verify versions before building
cat app.json | grep '"version"'
# -> "version": "1.0.0"
# -> "versionCode": 1
# -> "buildNumber": "1"

# Build both platforms in parallel
eas build --platform ios --profile production &
eas build --platform android --profile production &
wait

# Or use the combined command:
eas build --platform all --profile production

# After builds complete, check status:
eas build:list --status finished --limit 2

Smoke Test the Production Build

Install the exact production binary on a real device before submitting for review. Download the iOS .ipa to TestFlight and the Android .aab to Play Console internal testing. Test the sign-up flow, core feature, push notifications, and purchase flow (if applicable) from scratch — never assume the development version behavior matches the production build. Production builds have different signing, minification, and ProGuard settings that can expose bugs not seen in development.

// Pre-submission smoke test checklist:

// Install production build on physical device
// (not simulator — push notifications require real device)

// Test:
// [ ] Fresh install and sign up flow
// [ ] Sign out and sign in with existing account
// [ ] Core feature: create, complete, view streak
// [ ] Push notification: schedule and receive
// [ ] Background behavior: close and reopen app
// [ ] Deep links from browser/email
// [ ] Offline: airplane mode then restore network
// [ ] Landscape orientation (if not locked)
// [ ] All tabs and major screens load without crash
// [ ] Performance: no jank on FlatList scroll

Submitting to Both Stores

Submit to both stores simultaneously so review timelines align and you can launch on both platforms on the same day. Use eas submit for both. App Store review typically takes 1–3 days; Google Play takes 1–7 days for a first app (faster for updates). Have marketing assets ready (app website, social posts, press kit) so you can announce launch the moment both stores approve. Monitor both stores' review dashboards daily during the review period.

# Submit to App Store (TestFlight first, then App Review)
eas submit --platform ios --latest

# Submit to Play Console (internal -> production)
eas submit --platform android --latest

# Or submit both at once:
eas submit --platform all --latest

# After submission:
# iOS: App Store Connect > App > 1.0 Prepare for Submission
#      > Add for Review > Submit to App Review
# Android: Play Console > Production > Create release
#           > Promote internal build > Review > Start rollout

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to audit and polish your app with design tokens, accessibility labels, and minimum touch targets, how to write unit tests for streak logic and component tests with RNTL, and how to write a Maestro E2E flow for the critical user path and submit to both the App Store and Play Store. Congratulations — you have completed the React Native Mobile Development track! You now have the skills to build, test, and ship production-quality React Native apps to both platforms.

Frequently asked questions

Is the “Polishing, Testing, and Shipping to Both Stores” lesson free?

Yes — the full text of “Polishing, Testing, and Shipping to Both Stores” is free to read here on the web, and the React Native Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Native Academy course, upgrade to CoddyKit PRO.

What will I learn in “Polishing, Testing, and Shipping to Both Stores”?

Write unit tests for core hooks, add a Maestro E2E flow for the critical path, optimize the bundle, generate store assets, and submit to both the App Store and Google Play. You practise React Native Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Native Academy?

No prior experience is required. React Native Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Polishing, Testing, and Shipping to Both Stores” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Native Academy lesson?

Yes. Every React Native Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Planning Architecture and Tech Stack
  2. Authentication Flow and Protected Routes
  3. Core Feature: Data Feed with Offline Support
  4. Polishing, Testing, and Shipping to Both Stores
← Back to React Native Academy