Skip to content

polarsource/polar-react-native-starter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Polar React Native Starter

Setup a simple backend

import dotenv from "dotenv";
dotenv.config();

import { Hono } from "hono";
import { Checkout } from "@polar-sh/hono";
import { streamText } from "ai";
import { Ingestion } from "@polar-sh/ingestion";
import { LLMStrategy } from "@polar-sh/ingestion/strategies/LLM";
import { openai } from "@ai-sdk/openai";
import { serve } from "@hono/node-server";
import { hasSufficientCredits } from "./middlewares.js";
import { polarConfig } from "./polar.js";

const app = new Hono();

/// --- Checkout --- ///

app.get(
  "/checkout",
  Checkout({
    ...polarConfig,
    successUrl: process.env.POLAR_SUCCESS_URL,
  })
);

/// --- LLM --- ///

const llmIngestion = Ingestion(polarConfig)
  .strategy(new LLMStrategy(openai("gpt-4o")))
  .ingest("openai-usage");

app.post("/prompt", hasSufficientCredits, async (c) => {
  // You should obviously get this from an auth middleware or similar
  // but for this example we'll just use a fixed customer id
  const customerId = "09b8b19b-ff4a-4b3a-b12d-78ab168bf7bb";

  const { messages } = await c.req.json();

  const result = await streamText({
    model: llmIngestion.client({
      customerId,
    }),
    system: "You are a helpful assistant.",
    messages,
  });

  return result.toDataStreamResponse({
    headers: {
      "Content-Type": "application/octet-stream",
      "Content-Encoding": "none",
    },
  });
});

app.get("/checkout_redirect", (c) => {
  // Redirect to the app

  // Use the .well-known/apple-app-site-association file to redirect to your app instead
  // This is just a small hack for the sake of the demo
  return c.redirect("exp://172.22.79.116:8081?checkout_redirect");
});

serve({
  port: 8787,
  fetch: app.fetch,
});

Environment Variables

POLAR_ACCESS_TOKEN=...
POLAR_SUCCESS_URL=https://your-domain.com/checkout_redirect
POLAR_USAGE_METER_ID=...
OPENAI_API_KEY=...

Add a credits check

In order to make sure that users have sufficient credits, we can implement a simple middleware.

import { createMiddleware } from "hono/factory";
import { polar } from "./polar.js";

// You should obviously get this from an auth middleware or similar
// but for this example we'll just use a fixed customer id
const customerId = "09b8b19b-ff4a-4b3a-b12d-78ab168bf7bb";

export const hasSufficientCredits = createMiddleware(async (c, next) => {
  const meterId = process.env.POLAR_USAGE_METER_ID ?? "";

  const customerMeter = await polar.customerMeters.list({
    customerId,
    meterId,
  });

  const hasCredits = customerMeter.result.items.some(
    (customerMeter) => customerMeter.balance > 0
  );

  if (!hasCredits) {
    return c.json({
      error: "Insufficient credits",
      status: 400,
    });
  }

  await next();
});

Register App Links

Apple iOS

Add a file to your domain at .well-known/apple-app-site-association to define the URLs your app handles. Prepend your App ID with your Team ID, which you can find on the Membership page of the Apple Developer Portal.

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appIDs": [
          "XXXXXXXXXX.com.example.MyApp1",
          "XXXXXXXXXX.com.example.MyApp1-Debug"
        ],
        "components": [
          {
            "/checkout_redirect": "/checkout_redirect*",
            "comment": "Matches any URL whose path starts with /checkout_redirect"
          }
        ]
      }
    ]
  }
}

Android

You can learn more about adding App Links to your Expo Project here.

Implement the flow in React Native

import {
  StyleSheet,
  TouchableOpacity,
  TextInput,
  FlatList,
  KeyboardAvoidingView,
  Platform,
  SafeAreaView,
} from "react-native";
import * as Linking from "expo-linking";
import { fetch as expoFetch } from "expo/fetch";
import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { useColorScheme } from "@/hooks/useColorScheme";
import { useChat } from "@ai-sdk/react";
import { useEffect, useRef } from "react";

export default function HomeScreen() {
  const listRef = useRef<FlatList>(null);
  const isDark = useColorScheme() === "dark";

  const currentUrl = Linking.useURL();

  const { messages, handleInputChange, input, handleSubmit, setMessages } =
    useChat({
      fetch: expoFetch as unknown as typeof globalThis.fetch,
      api: process.env.EXPO_PUBLIC_API_BASE_URL + "/prompt",
      onError: (error) => {
        setMessages((messages) => [
          ...messages,
          {
            id: Math.random().toString(),
            role: "assistant",
            content: "You have insufficient credits.",
          },
        ]);
      },
    });

  useEffect(() => {
    if (currentUrl?.includes("?checkout_redirect")) {
      setMessages((messages) => [
        ...messages,
        {
          id: Math.random().toString(),
          role: "assistant",
          content:
            "Thanks for your purchase! 10,000 credits has been added to your account.",
        },
      ]);
    }
  }, [currentUrl, setMessages]);

  const renderMessage = ({ item }: { item: (typeof messages)[number] }) => (
    <ThemedView
      style={[
        styles.messageContainer,
        item.role === "user" ? styles.userMessage : styles.botMessage,
        item.role === "user" && {
          backgroundColor: isDark ? "#222" : "#F0F0F0",
        },
      ]}
    >
      <ThemedText>{item.content}</ThemedText>
      {item.content.includes("insufficient credits") && (
        <ThemedText
          style={styles.link}
          onPress={() => {
            // We're using a fixed Customer ID in the case of this demo
            const customerId = "09b8b19b-ff4a-4b3a-b12d-78ab168bf7bb";
            const polarCreditsProductId =
              "6870a5f6-1ff8-4907-8fe4-52c5c492f65b";

            const url = new URL(
              process.env.EXPO_PUBLIC_API_BASE_URL + "/checkout"
            );

            url.searchParams.set("products", polarCreditsProductId);
            url.searchParams.set("customerId", customerId);
            Linking.openURL(url.toString());
          }}
        >
          Please top up your account to continue.
        </ThemedText>
      )}
    </ThemedView>
  );

  useEffect(() => {
    listRef.current?.scrollToEnd();
  }, [messages]);

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === "ios" ? "padding" : "height"}
      style={styles.keyboardAvoid}
    >
      <SafeAreaView style={styles.container}>
        <ThemedView style={styles.container}>
          <FlatList
            ref={listRef}
            data={messages}
            renderItem={renderMessage}
            keyExtractor={(item) => item.id}
            style={styles.listContainer}
            contentInset={{ bottom: 12 }}
          />

          <ThemedView
            style={StyleSheet.flatten([
              styles.inputContainer,
              { backgroundColor: isDark ? "#222" : "#F0F0F0" },
            ])}
          >
            <TextInput
              style={StyleSheet.flatten([
                styles.input,
                { color: isDark ? "#fff" : "#000" },
              ])}
              value={input}
              onChange={(e) =>
                handleInputChange({
                  ...e,
                  target: {
                    ...e.target,
                    value: e.nativeEvent.text,
                  },
                } as unknown as React.ChangeEvent<HTMLInputElement>)
              }
              onSubmitEditing={(e) => {
                handleSubmit(e);
                e.preventDefault();
              }}
              placeholder="Ask anything..."
              placeholderTextColor={isDark ? "#666" : "#aaa"}
              autoFocus
            />
            <TouchableOpacity
              style={styles.sendButton}
              onPress={handleSubmit}
              activeOpacity={0.8}
            >
              <ThemedText
                style={styles.buttonText}
                lightColor="#fff"
                darkColor="#fff"
              ></ThemedText>
            </TouchableOpacity>
          </ThemedView>
        </ThemedView>
      </SafeAreaView>
    </KeyboardAvoidingView>
  );
}

Environment Variables

EXPO_PUBLIC_POLAR_SERVER_ENVIRONMENT=sandbox
EXPO_PUBLIC_API_BASE_URL=https://your-domain.com

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published