---
title: "In-app subscriptions in a Twitter clone made easy: a RevenueCat + Stream tutorial"
description: "Learn how to build a fully functioning app using SwiftUI, with in-app subscriptions, in a weekend. "
language: "en"
publishedAt: "2023-05-18T08:30:01Z"
updatedAt: "2023-05-18T08:30:01Z"
authors:
  - name: "Peter Meinertzhagen"
    url: "https://www.revenuecat.com/blog/author/peter-meinertzhagen"
category: "Engineering"
categoryUrl: "https://www.revenuecat.com/blog/engineering"
readingTime: 5
canonical: "https://www.revenuecat.com/blog/engineering/twitter-clone-with-revenuecat-stream-tutorial"
---

# In-app subscriptions in a Twitter clone made easy: a RevenueCat + Stream tutorial

Learn how to build a fully functioning app using SwiftUI, with in-app subscriptions, in a weekend. 

## Table of contents

- [1. Create a RevenueCat account](#1-create-a-revenuecat-account)
- [2. Create a project on the dashboard and select the platform of your app](#2-create-a-project-on-the-dashboard-and-select-the-platform-of-your-app)
- [3. Configure your product in App Store Connect](#3-configure-your-product-in-app-store-connect)
- [4. Configure your product in RevenueCat](#4-configure-your-product-in-revenuecat)
- [5. Install and configure the RevenueCat SDK](#5-install-and-configure-the-revenuecat-sdk)
- [That’s it! Told you it would be quick](#thats-it-told-you-it-would-be-quick)

Last month, Stream launched their superb tutorial project, [teaching you how to build a Twitter clone with Swift UI in just a weekend](https://getstream.io/resources/projects/twitter-clone/).

This resource teaches developers a bunch of things. Firstly, if you’ve ever wanted to learn more about iOS and SwiftUI development, then this is a quick self-contained project that will give you something tangible by the end of it. Secondly, it shows all developers the power of using third-party platforms to power discreet functionality in your app.

[Stream](https://getstream.io/) for chat and activity feeds, [Algolia](https://www.algolia.com/) for search, [100ms](https://www.100ms.live/) for audio rooms, [Mux](https://www.mux.com/) for video playback, and RevenueCat for subscriptions. Powering up your project with these platforms is the only way you’d get this done, with little sweat, in a weekend.

In our portion of this tutorial, we’re going to spotlight the in-app subscriptions step of the project using RevenueCat. To get started, download the source code and project demo from [Github](https://github.com/GetStream/twitter-clone/). If you haven’t already, you’ll want to check out [parts 1-6](https://getstream.io/resources/projects/twitter-clone/) of the tutorial series first.

This tutorial is broken down into five parts:

1. Create a RevenueCat account.

2. Create a project on the dashboard and select the platform of your app.

3. Configure your product in App Store Connect.

4. Configure your product in RevenueCat.

5. Install and configure the RevenueCat SDK.

As we go, we’ll be highlighting the key things to know as we progress through configuring your in-app subscription — please refer to our [SDK quick-start](https://www.revenuecat.com/docs/getting-started) and [SDK configuration](https://www.revenuecat.com/docs/configuring-sdk) guides for further resources.

## 1. Create a RevenueCat account

[Sign up for a new RevenueCat account here.](https://app.revenuecat.com/signup)

It’s worth bearing in mind for the future that we recommend setting up a separate RevenueCat account for each unique project or app you work on. Should you wish to sell or transfer ownership down the line, this makes it easier.

![](https://cdn.sanity.io/images/c3qnx9b0/production/d968de16957569fd3296007ac4657b827868a2c9-455x626.png)

*Figure 1. Create your RevenueCat account*

## 2. Create a project on the dashboard and select the platform of your app

Navigate to the RevenueCat dashboard and add a new project from the dropdown in the top navigation menu called Projects.

![](https://cdn.sanity.io/images/c3qnx9b0/production/65b16fac30d96a636fdd9eb85bf1848cf96adae9-555x351.png)

*Figure 2. Create your project within the RevenueCat dashboard*

Next you need to add an app. Choose ‘App Store’ as your platform and give your app a name. We’ll add the rest of the details later.

![](https://cdn.sanity.io/images/c3qnx9b0/production/3d96338d9531a9b2831b2a38bd4259861b7f205f-961x522.png)

*Figure 3. Add your app within your RevenueCat project*

## 3. Configure your product in App Store Connect

Next we’ll set up our Twitter Blue subscription in App Store Connect.

Rather than copy and paste the same information, you can find full details on how to set your product up in App Store Connect in our [iOS product setup guide](https://www.revenuecat.com/docs/ios-products).

Follow the instructions that guide. When you create your Subscription, make a note of your Product ID — we’ll be using that in the next step.

![](https://cdn.sanity.io/images/c3qnx9b0/production/f3832aaa420c0dd9ec2b7d8ca133f8717a65eaf1-1272x672.png)

*Figure 4. Create your Subscription in App Store Connect*

## 4. Configure your product in RevenueCat

Next up, we’ll return to the RevenueCat dashboard and configure the product you just set up in App Store Connect.

Once again, we have a [very comprehensive tutorial](https://www.revenuecat.com/docs/entitlements) that covers the ins and outs of product configuration in RevenueCat. We recommend that you give that a read. However, one thing to keep in mind, when you create your *entitlement* in RevenueCat, call it “blue” to match Stream’s SDK configuration guide.

Other than that, go ahead and match your App Store Connect product to a new one in RevenueCat.

## 5. Install and configure the RevenueCat SDK

We have guides outlining how to [install](https://www.revenuecat.com/docs/ios) and [configure](https://www.revenuecat.com/docs/configuring-sdk) the RevenueCat SDK for iOS (and [other platforms](https://www.revenuecat.com/docs/installation)).

However, for the purposes of this tutorial, we’ll be using the guide and code that Stream has already created. This method of integration uses [Tuist](https://tuist.io/), which we don’t cover in our documentation.

First of all, add the RevenueCat dependencies as a Swift Package to your Tuist *dependencies.swift* file.

```swift
var swiftPackageManagerDependencies = SwiftPackageManagerDependencies(
		[
     \u2026
		 .remote(url: "https:\/\/github.com\/RevenueCat\/purchases-ios.git", requirement: .range(from: "4.0.0", to: "5.0.0")),     \u2026
    ],
    productTypes: [
        \u2026
        "RevenueCat": .framework,
        \u2026
    ]
)
```

Then add the dependency in the Tuist project, in *project.swift*, to a target.

```swift
let profileTarget =
    Project.makeFrameworkTargets(name: profileName,
                                 platform: .iOS,
                                 dependencies: [
                                    .target(name: authName),
                                    .target(name: authUiName),
                                    .target(name: messagesName),
                                    .target(name: uiName),
                                    .external(name: "RevenueCat")
                                 ])

```

Now fetch the dependency by running:

```swift
tuist fetch
```

And then generate the Xcode project again by running:

```swift
tuist generate
```

From Stream’s codebase, you can find and explore how they implemented RevenueCat in the following Swift files in the folders Profile -> Sources from the Project Navigator.

*PurchaseViewModel.swift*

```swift
\/\/
\/\/  PurchaseManager.swift
\/\/  Profile
\/\/
\/\/  Created by Jeroen Leenarts on 13\/02\/2023.
\/\/  Copyright \u00a9 2023 Stream.io Inc. All rights reserved.
\/\/
\u200b
import SwiftUI
import Foundation
import RevenueCat
import Auth
\u200b
public class PurchaseViewModel: ObservableObject {
    @Published
    var offerings: Offerings?
    
    @Published
    var isSubscriptionActive = false
\u200b
    public func configure(userId: String?) {
        let configuration = Configuration.Builder(withAPIKey: "appl_ffoirKXwYnVnlhIlEaExRfMZxxf")
            .with(appUserID: userId)
            .build()
        Purchases.configure(with: configuration)
        
        Purchases.shared.getCustomerInfo { customerInfo, error in
            self.isSubscriptionActive = customerInfo?.entitlements.all["blue"]?.isActive == true
            if let error {
                print(error)
            }
\u200b
        }
        
        Purchases.shared.getOfferings { offerings, error in
            if let offerings {
                self.offerings = offerings
            }
            if let error {
                print(error)
            }
        }
    }
}
```

From the code above, the SDK is first initialized using the public API key obtained from your RevenueCat project settings and the shared instance of the RevenueCat SDK is configured during the app launch. For any subscription app, it is necessary to get the users’ subscription status. The RevenueCat SDK makes this easy using the customer information object returned from the Purchases.shared.getCustomerInfo method as shown in the code above.

Next, the code retrieves information about TwitterClone’s available in-app purchases from App Store Connect using the Purchases.shared.getOfferings method. You can then display those available products in your TwitterClone paywall.

*SubscribeBlue.swift*

In your SubscribeBlue.swift SwiftUI view, add a property and an init method that accepts a Package from the Offerings object in the previous step. Every Offering needs a Package, which is simply a group of equivalent products across iOS, Android, and web — however, as we’re only dealing with one platform here, our Package contains only iOS products.

```swift
var package: Package
    public init(package: Package) {
        self.package = package
    }
```

Then, implement the code sample below demonstrating purchasing a package using a subscribe button.

```swift
Button {
          Purchases.shared.purchase(package: package) { transaction, customerInfo, error, userCancelled in
             if customerInfo?.entitlements.all["blue"]?.isActive == true {
                  print("Bought")
             }
         }

      } label: {
          Text("Subscribe for $2.99\/month")
          Image(systemName: "checkmark.seal.fill")
      }
      .buttonStyle(.bordered)
      .padding(.top, 32)

```

See the full implementation in the code sample below.

*SubscribeBlue.swift*

```swift
import SwiftUI
import RevenueCat

public struct SubscribeBlue: View {
    var package: Package
    public init(package: Package) {
        self.package = package
    }
    
    @State private var isShowingConfirmSheet = false
    
    public var body: some View {
        NavigationStack {
            VStack {
                VStack(alignment: .leading, spacing: 16) {
                    Text("Get more out of Twitter Blue with exclusive features")
                        .font(.title)
                        .multilineTextAlignment(.center)
                        .padding(.bottom)
                    
                    HStack(alignment: .top) {
                        Image(systemName: "circle.slash")
                        VStack(alignment: .leading) {
                            Text("Ad-free articles")
                                .font(.headline)
                            Text("Read ad-free articles from popular websites with no pay walls")
                        }
                    }
                    
                    HStack(alignment: .top) {
                        Image(systemName: "flame")
                        VStack(alignment: .leading) {
                            Text("Top articles")
                                .font(.headline)
                            Text("Read ad-free articles from popular websites with no pay walls")
                        }
                    }
                    
                    HStack(alignment: .top) {
                        Image(systemName: "pin.circle")
                        VStack(alignment: .leading) {
                            Text("Custom navigation")
                                .font(.headline)
                            Text("Read ad-free articles from popular websites with no pay walls")
                        }
                    }
                    
                    HStack(alignment: .top) {
                        Image(systemName: "theatermask.and.paintbrush")
                        VStack(alignment: .leading) {
                            Text("Custom app icon and themes")
                                .font(.headline)
                            Text("Read ad-free articles from popular websites with no pay walls")
                        }
                    }
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        HStack {
                            Image(systemName: "checkmark.seal.fill")
                            Text("Blue")
                        }
                        .foregroundColor(.streamBlue)
                        
                    }
                }
                Button { \/\/ Make a purchase
                    Purchases.shared.purchase(package: package) { transaction, customerInfo, error, userCancelled in
                        if customerInfo?.entitlements.all["blue"]?.isActive == true {
                            print("Bought")
                        }
                    }

                } label: {
                    Text("Subscribe for $2.99\/month")
                    Image(systemName: "checkmark.seal.fill")
                }
                .buttonStyle(.bordered)
                .padding(.top, 32)
            }
        }
    }
}

```

When the subscription is successful, the user gets the confirmation message “You are subscribed.” Otherwise, we send the customer to the subscription screen (*SubscribeBlue.swift*).

```swift
if purchaseViewModel.isSubscriptionActive {
                    Text("You are subscribed")
                        .padding(.top)
                } else {
                    if let packages = purchaseViewModel.offerings?.current?.availablePackages {
                        ForEach(packages) { package in
                            SubscribeBlue(package: package)
                        }
                    }
                }

```

See the full implementation in Stream’s *SettingsView.swift* file.

```swift
import SwiftUI
import TwitterCloneUI
import AuthUI
import Auth
import Feeds
import Chat
import DirectMessages
import RevenueCat

public struct SettingsView: View {
    @EnvironmentObject var feedsClient: FeedsClient
    @EnvironmentObject var auth: TwitterCloneAuth
    @EnvironmentObject var chatModel: ChatModel
    @EnvironmentObject var purchaseViewModel: PurchaseViewModel
    @Environment(\.presentationMode) var presentationMode
    
    @StateObject var mediaPickerViewModel = MediaPickerViewModel()
    
    @State private var isEditingName = "Amos Gyamfi"
    @State private var isEditingUserName = false
    @State private var isEditingPassword = false
    @State private var isLoggedOut = false
    public init () {}
    
    public var body: some View {
        NavigationStack {
            List {
                HStack {
                    Button {
                        print("Open the photo picker")
                    } label: {
                        HStack {
                            ZStack {
                                ProfileImage(imageUrl: "https:\/\/picsum.photos\/id\/64\/200", action: {})
                                    .opacity(0.6)
                                MediaPickerView(viewModel: mediaPickerViewModel)
                            }
                            Image(systemName: "pencil")
                                .fontWeight(.bold)
                        }
                    }
                    
                    Spacer()
                }
                
                HStack {
                    Text("Change your Name")
                    TextField("Amos Gyamfi", text: $isEditingName)
                        .foregroundColor(.streamBlue)
                        .labelsHidden()
                }
                
                NavigationLink {
                    EditUserName()
                } label: {
                    Button {
                        self.isEditingUserName.toggle()
                    } label: {
                        HStack {
                            Text("Change your username")
                            Spacer()
                            Text("@stefanjblos")
                        }
                    }
                }
                
                NavigationLink {
                    EditPassword()
                } label: {
                    Button {
                        self.isEditingPassword.toggle()
                    } label: {
                        HStack {
                            Text("Change your password")
                            Spacer()
                        }
                    }
                }

                if purchaseViewModel.isSubscriptionActive {
                    Text("You are subscribed")
                        .padding(.top)
                } else {
                    if let packages = purchaseViewModel.offerings?.current?.availablePackages {
                        ForEach(packages) { package in
                            SubscribeBlue(package: package)
                        }
                    }
                }
            }
            .listStyle(.plain)
            .navigationTitle("")
            .navigationBarTitleDisplayMode(.inline)
            .frame(maxHeight: 280)
            .toolbar {
                ToolbarItem(placement: .principal) {
                    Text("Your acount settings")
                }
            }
            
            Button(role: .destructive) {
                presentationMode.wrappedValue.dismiss()
                auth.logout()
\/\/                chatModel.logout()
            } label: {
                Image(systemName: "power.circle.fill")
                Text("Log out")
            }
            
            Spacer()
        }
    }
}

```

## That’s it! Told you it would be quick

At this point, we’d recommend that you go and read [Stream’s tutorial wrap-up](https://getstream.io/resources/projects/twitter-clone/wrap-up-and-conclusion/).

And if you’re itching to complete more tutorials, then you might be interested in:

- our [guide to building a SwiftUI app with in-app subscriptions with StoreKit 2](https://revenuecat.com/blog/engineering/ios-in-app-subscription-tutorial-with-storekit-2-and-swift/) (and although you’ll quickly come to the conclusion that it was easier with RevenueCat all along, you’ll nevertheless pick up some useful knowledge)
- our [guide to implementing in-app subscriptions in an Android app](https://revenuecat.com/blog/engineering/android-in-app-subscriptions-tutorial/) (using RevenueCat)
- and our [guide to implementing in-app purchases in a React Native app with Expo](https://revenuecat.com/blog/engineering/expo-in-app-purchase-tutorial/)
