Skip to content

Commit

Permalink
Proper stripe sync (#80)
Browse files Browse the repository at this point in the history
* update migrations

* use deno.json properly

* update script to get database types and have it update edge function types as well

* update backend deno with stripe webhook and stripe functions

* update logic for get_stripe_url

* stripe test webhooks & make sure edge functions are working locally

* make nextjs work with new stripe sync backend

* change auth redirect to a rewrite to make it load faster

* refresh page to update account when signed out

* remove redundant import

* add current subscription tier to nextjs

* make flutter compatible with stripe sync

* add See Pricing button

* add pricing to readme

* update env examples to point to supabase

* remove unnecessary variable in .env.example

* fix env vars and update pleasereply to hi for email
  • Loading branch information
matthewwong525 authored Jul 19, 2024
1 parent 969b666 commit 0ff615f
Show file tree
Hide file tree
Showing 40 changed files with 1,876 additions and 395 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ GOOGLE_SECRET="YOUR_GOOGLE_SECRET"

GITHUB_CLIENT_ID="YOUR_GITHUB_CLIENT_ID"
GITHUB_SECRET="YOUR_GITHUB_SECRET"

STRIPE_SECRET_KEY="YOUR_STRIPE_SECRET_KEY"
STRIPE_WEBHOOK_SIGNING_SECRET="YOUR_STRIPE_WEBHOOK_SIGNING_SECRET"
POSTHOG_CLIENT_KEY="YOUR_POSTHOG_CLIENT_KEY"
POSTMARK_SERVER_TOKEN="YOUR_POSTMARK_SERVER_TOKEN"
POSTMARK_FROM_EMAIL="YOUR_POSTMARK_FROM_EMAIL"
9 changes: 8 additions & 1 deletion flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ cd flutter
flutter run -d chrome --dart-define-from-file=env.json
```

## Pricing

You would normally put your pricing page on your landing page. Below are links to try out how the pricing works in the app.

- [Hobby Plan ($10 / month)](https://flutter.devtodollars.com/payments?price=price_1Pdy8yFttF99a1NCLpDa83xf)
- [Freelancer Plan ($20 / month)](https://flutter.devtodollars.com/payments?price=price_1Pdy8zFttF99a1NCGQJc5ZTZ)

## Stack

- State Management ([riverpod](https://pub.dev/packages/riverpod))
- Routing ([go\_router](https://pub.dev/packages/go\_router))
- Routing ([go_router](https://pub.dev/packages/go_router))
4 changes: 2 additions & 2 deletions flutter/env.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"SUPABASE_URL": "https://crnytzptlghehxsarjxm.supabase.co",
"SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNybnl0enB0bGdoZWh4c2FyanhtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMjQxNjgsImV4cCI6MjAyNDgwMDE2OH0.UW1dHRt4hGF6uCdPXimxv0Ggwq5uJ1WoQuCZ1_ixmCU"
"SUPABASE_URL": "https://ibyxomczmyaptsjgzvzi.supabase.co",
"SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlieXhvbWN6bXlhcHRzamd6dnppIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjEzMDk0OTUsImV4cCI6MjAzNjg4NTQ5NX0.DOhoJJ3V7KJFjglNftgDDQwMNrB3oiWtf21BfRcB2Cc"
}
2 changes: 1 addition & 1 deletion flutter/lib/components/email_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class _EmailFormState extends ConsumerState<EmailForm> {
builder: (_) => AlertDialog(
title: const Text("Check your Email!"),
content: const Text(
"We sent an email from pleasereply@devtodollars.com to verify your email"),
"We sent an email from hi@devtodollars.com to verify your email"),
actions: [
TextButton(
onPressed: context.pop, child: const Text("Ok Matt."))
Expand Down
15 changes: 0 additions & 15 deletions flutter/lib/models/app_user.dart

This file was deleted.

186 changes: 186 additions & 0 deletions flutter/lib/models/stripe.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
class SubscriptionWithPrice {
final String? cancelAt;
final bool? cancelAtPeriodEnd;
final String? canceledAt;
final String created;
final String currentPeriodEnd;
final String currentPeriodStart;
final String? endedAt;
final String id;
final Map<String, dynamic>? metadata;
final String? priceId;
final int? quantity;
final String? status;
final String? trialEnd;
final String? trialStart;
final String userId;
final PriceWithProduct? prices;

SubscriptionWithPrice({
this.cancelAt,
this.cancelAtPeriodEnd,
this.canceledAt,
required this.created,
required this.currentPeriodEnd,
required this.currentPeriodStart,
this.endedAt,
required this.id,
this.metadata,
this.priceId,
this.quantity,
this.status,
this.trialEnd,
this.trialStart,
required this.userId,
this.prices,
});

factory SubscriptionWithPrice.fromJson(Map<String, dynamic> json) {
return SubscriptionWithPrice(
cancelAt: json['cancel_at'],
cancelAtPeriodEnd: json['cancel_at_period_end'],
canceledAt: json['canceled_at'],
created: json['created'],
currentPeriodEnd: json['current_period_end'],
currentPeriodStart: json['current_period_start'],
endedAt: json['ended_at'],
id: json['id'],
metadata: json['metadata'],
priceId: json['price_id'],
quantity: json['quantity'],
status: json['status'],
trialEnd: json['trial_end'],
trialStart: json['trial_start'],
userId: json['user_id'],
prices: json['prices'] != null
? PriceWithProduct.fromJson(json['prices'])
: null,
);
}

Map<String, dynamic> toJson() {
return {
'cancel_at': cancelAt,
'cancel_at_period_end': cancelAtPeriodEnd,
'canceled_at': canceledAt,
'created': created,
'current_period_end': currentPeriodEnd,
'current_period_start': currentPeriodStart,
'ended_at': endedAt,
'id': id,
'metadata': metadata,
'price_id': priceId,
'quantity': quantity,
'status': status,
'trial_end': trialEnd,
'trial_start': trialStart,
'user_id': userId,
'prices': prices?.toJson(),
};
}
}

class PriceWithProduct {
final bool? active;
final String? currency;
final String? description;
final String id;
final String? interval;
final int? intervalCount;
final Map<String, dynamic>? metadata;
final String? productId;
final int? trialPeriodDays;
final String? type;
final int? unitAmount;
final Product? products;

PriceWithProduct({
this.active,
this.currency,
this.description,
required this.id,
this.interval,
this.intervalCount,
this.metadata,
this.productId,
this.trialPeriodDays,
this.type,
this.unitAmount,
this.products,
});

factory PriceWithProduct.fromJson(Map<String, dynamic> json) {
return PriceWithProduct(
active: json['active'],
currency: json['currency'],
description: json['description'],
id: json['id'],
interval: json['interval'],
intervalCount: json['interval_count'],
metadata: json['metadata'],
productId: json['product_id'],
trialPeriodDays: json['trial_period_days'],
type: json['type'],
unitAmount: json['unit_amount'],
products:
json['products'] != null ? Product.fromJson(json['products']) : null,
);
}

Map<String, dynamic> toJson() {
return {
'active': active,
'currency': currency,
'description': description,
'id': id,
'interval': interval,
'interval_count': intervalCount,
'metadata': metadata,
'product_id': productId,
'trial_period_days': trialPeriodDays,
'type': type,
'unit_amount': unitAmount,
'products': products?.toJson(),
};
}
}

class Product {
final bool? active;
final String? description;
final String id;
final String? image;
final Map<String, dynamic>? metadata;
final String? name;

Product({
this.active,
this.description,
required this.id,
this.image,
this.metadata,
this.name,
});

factory Product.fromJson(Map<String, dynamic> json) {
return Product(
active: json['active'],
description: json['description'],
id: json['id'],
image: json['image'],
metadata: json['metadata'],
name: json['name'],
);
}

Map<String, dynamic> toJson() {
return {
'active': active,
'description': description,
'id': id,
'image': image,
'metadata': metadata,
'name': name,
};
}
}
43 changes: 43 additions & 0 deletions flutter/lib/models/user_metadata.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:devtodollars/models/stripe.dart';

class UserMetadata {
final String? avatarUrl;
final Map<String, dynamic>? billingAddress;
final String? fullName;
final String id;
final Map<String, dynamic>? paymentMethod;
SubscriptionWithPrice? subscription;

UserMetadata({
this.avatarUrl,
this.billingAddress,
this.fullName,
required this.id,
this.paymentMethod,
this.subscription,
});

factory UserMetadata.fromJson(Map<String, dynamic> json) {
return UserMetadata(
avatarUrl: json['avatar_url'],
billingAddress: json['billing_address'],
fullName: json['full_name'],
id: json['id'],
paymentMethod: json['payment_method'],
subscription: json['subscription'] != null
? SubscriptionWithPrice.fromJson(json['subscription'])
: null,
);
}

Map<String, dynamic> toJson() {
return {
'avatar_url': avatarUrl,
'billing_address': billingAddress,
'full_name': fullName,
'id': id,
'payment_method': paymentMethod,
'subscription': subscription?.toJson(),
};
}
}
2 changes: 1 addition & 1 deletion flutter/lib/screens/auth_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class _AuthScreenState extends State<AuthScreen> {
builder: (_) => AlertDialog(
title: const Text("Check your Email!"),
content: const Text(
"We sent an email from pleasereply@devtodollars.com to verify your email"),
"We sent an email from hi@devtodollars.com to verify your email"),
actions: [
TextButton(onPressed: context.pop, child: const Text("Ok Matt."))
],
Expand Down
36 changes: 18 additions & 18 deletions flutter/lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:devtodollars/services/metadata_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:devtodollars/services/auth_notifier.dart';
import 'package:url_launcher/url_launcher.dart';

class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key, required this.title});
Expand All @@ -13,17 +15,12 @@ class HomeScreen extends ConsumerStatefulWidget {
}

class _HomeScreenState extends ConsumerState<HomeScreen> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
final authNotif = ref.watch(authProvider.notifier);
final metaAsync = ref.watch(metadataProvider);
final pricingUrl = Uri.parse(
"https://github.com/devtodollars/mvp-boilerplate/blob/main/flutter/README.md");
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
Expand All @@ -40,21 +37,24 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
metaAsync.when(
data: (metadata) {
final subscription = metadata?.subscription;
return (Text(subscription != null
? "You are currently on the ${subscription.prices?.products?.name} plan."
: "You are not currently subscribed to any plan."));
},
loading: () => const CircularProgressIndicator(),
error: (_, __) => const Text("Failed to load subscription plan"),
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
const SizedBox(height: 8),
TextButton(
onPressed: () => launchUrl(pricingUrl),
child: const Text("See Pricing"),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Loading

0 comments on commit 0ff615f

Please sign in to comment.