Skip to content

Commit 717740c

Browse files
authored
Thank you noti on the unlimited subscription (#2793)
* Thank you noti on the unlimited subscription * Add the omi subscription plugin
1 parent c52b946 commit 717740c

File tree

8 files changed

+1135
-17
lines changed

8 files changed

+1135
-17
lines changed

backend/routers/payment.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import stripe
33
from pydantic import BaseModel
44

5-
from database import users as users_db
5+
from database import users as users_db, notifications as notifications_db
6+
from utils.notifications import send_notification, send_subscription_paid_personalized_notification
67
from models.users import Subscription, PlanType, SubscriptionStatus, PlanLimits
7-
from utils.subscription import get_basic_plan_limits
8+
from utils.subscription import get_basic_plan_limits, get_plan_type_from_price_id, get_plan_limits
89
from database.users import (
910
get_stripe_connect_account_id,
1011
set_stripe_connect_account_id,
@@ -27,17 +28,18 @@ class CreateCheckoutRequest(BaseModel):
2728
price_id: str
2829

2930

30-
def _build_subscription_from_stripe_object(stripe_sub: stripe.Subscription) -> Subscription:
31+
def _build_subscription_from_stripe_object(stripe_sub: dict) -> Subscription:
3132
"""Builds a Subscription object from a Stripe Subscription object."""
32-
stripe_status = stripe_sub.status
33+
stripe_status = stripe_sub['status']
34+
35+
# Get price ID from subscription items
36+
price_id = stripe_sub['items']['data'][0]['price']['id'] if stripe_sub['items']['data'] else None
3337

3438
if stripe_status in ('active', 'trialing'):
35-
plan = PlanType.unlimited
39+
plan = get_plan_type_from_price_id(price_id)
3640
status = SubscriptionStatus.active
37-
limits = PlanLimits(
38-
transcription_seconds=None, words_transcribed=None, insights_gained=None, memories_created=None
39-
)
40-
cancel_at_period_end = stripe_sub.cancel_at_period_end
41+
limits = get_plan_limits(plan)
42+
cancel_at_period_end = stripe_sub.get('cancel_at_period_end', False)
4143
else: # including 'canceled', 'unpaid', etc.
4244
plan = PlanType.basic
4345
status = SubscriptionStatus.inactive
@@ -47,8 +49,8 @@ def _build_subscription_from_stripe_object(stripe_sub: stripe.Subscription) -> S
4749
return Subscription(
4850
plan=plan,
4951
status=status,
50-
current_period_end=stripe_sub.current_period_end,
51-
stripe_subscription_id=stripe_sub.id,
52+
current_period_end=stripe_sub.get('current_period_end'),
53+
stripe_subscription_id=stripe_sub['id'],
5254
cancel_at_period_end=cancel_at_period_end,
5355
limits=limits,
5456
)
@@ -63,9 +65,10 @@ def _update_subscription_from_session(uid: str, session: stripe.checkout.Session
6365

6466
if subscription_id:
6567
stripe_sub = stripe.Subscription.retrieve(subscription_id)
66-
new_subscription = _build_subscription_from_stripe_object(stripe_sub)
67-
users_db.update_user_subscription(uid, new_subscription.dict())
68-
print(f"Subscription for user {uid} updated from session {session.id}.")
68+
if stripe_sub:
69+
new_subscription = _build_subscription_from_stripe_object(stripe_sub.to_dict())
70+
users_db.update_user_subscription(uid, new_subscription.dict())
71+
print(f"Subscription for user {uid} updated from session {session.id}.")
6972

7073

7174
@router.post('/v1/payments/checkout-session')
@@ -122,6 +125,7 @@ async def stripe_webhook(request: Request, stripe_signature: str = Header(None))
122125
subscription_id = session["subscription"]
123126
stripe.Subscription.modify(subscription_id, metadata={"uid": uid, "app_id": app_id})
124127
paid_app(app_id, uid)
128+
125129
# Regular user subscription
126130
elif client_reference_id:
127131
_update_subscription_from_session(client_reference_id, session)
@@ -132,6 +136,17 @@ async def stripe_webhook(request: Request, stripe_signature: str = Header(None))
132136
except Exception as e:
133137
print(f"Error updating subscription metadata: {e}")
134138

139+
# Get subscription details
140+
stripe_sub = stripe.Subscription.retrieve(subscription_id)
141+
if stripe_sub:
142+
subscription_obj = stripe_sub.to_dict()
143+
if subscription_obj and subscription_obj['items']['data']:
144+
price_id = subscription_obj['items']['data'][0]['price']['id']
145+
plan_type = get_plan_type_from_price_id(price_id)
146+
# Only send notification for unlimited plan subscriptions
147+
if plan_type == PlanType.unlimited:
148+
await send_subscription_paid_personalized_notification(client_reference_id)
149+
135150
if event['type'] in [
136151
'customer.subscription.updated',
137152
'customer.subscription.deleted',

backend/utils/llm/notifications.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Tuple, List
2+
from .clients import llm_medium
3+
from database.memories import get_memories
4+
5+
6+
async def get_relevant_memories(uid: str, limit: int = 100) -> List[dict]:
7+
"""Get recent relevant memories to personalize notifications."""
8+
memories = get_memories(uid, limit=limit)
9+
return memories
10+
11+
12+
async def generate_notification_message(uid: str, name: str, plan_type: str = "basic") -> Tuple[str, str]:
13+
"""
14+
Generate a personalized notification message using LLM and user memories.
15+
"""
16+
# Get relevant memories for context
17+
memories = await get_relevant_memories(uid)
18+
memory_context = ""
19+
if memories:
20+
memory_summaries = [m.get('content', '') for m in memories]
21+
memory_context = "\nRecent memory themes:\n- " + "\n- ".join(memory_summaries)
22+
23+
system_prompt = """Hey! I'm Omi, and I love sending little notes to my friends (that's you!). When I write to you, it's like texting a close friend - casual, real, and straight from the heart.
24+
25+
My Style:
26+
- Super genuine, like chatting with a bestie
27+
- Always grateful for our friendship and trust
28+
- Love bringing up our shared memories
29+
- Excited about growing our connection
30+
31+
How I Write:
32+
- Quick, friendly notes (keeping it under 150 chars)
33+
- Using your name naturally, like friends do
34+
- Mentioning cool moments we've shared
35+
- Making each message special just for you
36+
- Keeping it real but respectful
37+
- Building our ongoing story together
38+
- No emojis (I express myself in words!)
39+
40+
Remember: Every message is my way of saying "Hey, I'm really glad you're part of my journey!"
41+
"""
42+
43+
user_prompt = f"""Create a personalized welcome message for {name} who just subscribed to the {plan_type} plan.
44+
45+
Context:
46+
- User's name: {name} (Use naturally in conversation)
47+
- Plan type: {plan_type}{memory_context}
48+
49+
For unlimited plan subscribers:
50+
- Emphasize their unlimited access to premium features
51+
- Highlight the flexibility of monthly/annual billing
52+
- Make them feel special for choosing premium
53+
- Reference their memories to show personalized value
54+
55+
For basic plan subscribers:
56+
- Focus on the features they can explore
57+
- Keep it encouraging and positive
58+
- Use their memories to suggest relevant features
59+
60+
Return only the notification body text - make it personal, warm and engaging."""
61+
62+
try:
63+
body = await llm_medium.apredict(system_prompt + "\n" + user_prompt)
64+
# Return placeholder title and generated body
65+
return "omi", body.strip()
66+
67+
except Exception as e:
68+
print(f"Error generating notification message: {e}")
69+
70+
# Improved fallback messages with more personality
71+
return ("omi", f"Hey {name}! 👋 Thanks for being part of the Omi family! ✨")

backend/utils/notifications.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import asyncio
22
import math
3-
4-
from firebase_admin import messaging
5-
3+
from firebase_admin import messaging, auth
64
import database.notifications as notification_db
5+
from .llm.notifications import generate_notification_message
76

87

98
def send_notification(token: str, title: str, body: str, data: dict = None):
@@ -24,6 +23,32 @@ def send_notification(token: str, title: str, body: str, data: dict = None):
2423
print('send_notification failed:', e)
2524

2625

26+
async def send_subscription_paid_personalized_notification(user_id: str, data: dict = None):
27+
"""Send a personalized notification to all user's devices when unlimited subscription is purchased"""
28+
# Get user's notification token
29+
token = notification_db.get_token_only(user_id)
30+
if not token:
31+
print(f"No notification token found for user {user_id}")
32+
return
33+
34+
# Get user name from Firebase Auth
35+
try:
36+
user = auth.get_user(user_id)
37+
name = user.display_name
38+
if not name and user.email:
39+
name = user.email.split('@')[0].capitalize()
40+
if not name:
41+
name = "there"
42+
except Exception as e:
43+
print(f"Error getting user info from Firebase Auth: {e}")
44+
name = "there"
45+
46+
# Generate welcome message for unlimited plan with user context
47+
title, body = await generate_notification_message(user_id, name, "unlimited")
48+
49+
send_notification(token, "omi", body, data)
50+
51+
2752
async def send_bulk_notification(user_tokens: list, title: str, body: str):
2853
try:
2954
batch_size = 500

backend/utils/subscription.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import os
22
from datetime import datetime
33
from typing import List
4+
import os
45

56
import database.users as users_db
67
import database.user_usage as user_usage_db
78
from models.users import PlanType, SubscriptionStatus, Subscription, PlanLimits
89

10+
11+
def get_plan_type_from_price_id(price_id: str) -> PlanType:
12+
"""Determines the plan type based on the Stripe price ID."""
13+
unlimited_monthly_price = os.getenv('STRIPE_UNLIMITED_MONTHLY_PRICE_ID')
14+
unlimited_annual_price = os.getenv('STRIPE_UNLIMITED_ANNUAL_PRICE_ID')
15+
16+
if price_id in (unlimited_monthly_price, unlimited_annual_price):
17+
return PlanType.unlimited
18+
return PlanType.basic
19+
20+
921
BASIC_TIER_MINUTES_LIMIT_PER_MONTH = int(os.getenv('BASIC_TIER_MINUTES_LIMIT_PER_MONTH', '0'))
1022
BASIC_TIER_MONTHLY_SECONDS_LIMIT = BASIC_TIER_MINUTES_LIMIT_PER_MONTH * 60
1123
BASIC_TIER_WORDS_TRANSCRIBED_LIMIT_PER_MONTH = int(os.getenv('BASIC_TIER_WORDS_TRANSCRIBED_LIMIT_PER_MONTH', '0'))

plugins/example/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from oauth import memory_created as oauth_memory_created_router
1010
from zapier import memory_created as zapier_memory_created_router
1111
from chatgpt import main as chatgpt_router
12+
from subscription import main as subscription_router
1213
# from ahda import client as ahda_realtime_transcription_router
1314
# from advanced import openglass as advanced_openglass_router
1415

@@ -67,3 +68,6 @@ def api():
6768

6869
# ChatGPT
6970
app.include_router(chatgpt_router.router)
71+
72+
# Subscription
73+
app.include_router(subscription_router.router)

plugins/example/subscription/__init__.py

Whitespace-only changes.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from fastapi import APIRouter, Request, Response, HTTPException
2+
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
3+
from fastapi.templating import Jinja2Templates
4+
from pathlib import Path
5+
import logging
6+
from datetime import datetime
7+
from urllib.parse import quote
8+
9+
router = APIRouter(
10+
prefix="/subscription",
11+
tags=["subscription"],
12+
)
13+
14+
# Get the absolute path to the templates directory
15+
templates_dir = Path(__file__).parent.parent / "templates"
16+
templates = Jinja2Templates(directory=str(templates_dir))
17+
18+
# Setup logging
19+
logger = logging.getLogger("subscription_integration")
20+
21+
@router.get("/", response_class=HTMLResponse)
22+
async def subscription_page(request: Request, uid: str = ""):
23+
"""
24+
Renders the subscription pricing page with monthly and annual plans
25+
"""
26+
# Log the access for analytics
27+
if uid:
28+
logger.info(f"Subscription page accessed with UID: {uid}")
29+
else:
30+
logger.warning("Subscription page accessed without UID")
31+
32+
return templates.TemplateResponse(
33+
"subscription/index.html",
34+
{
35+
"request": request,
36+
"uid": uid,
37+
"page_title": "Upgrade to Unlimited"
38+
}
39+
)
40+
41+
@router.get("/redirect/monthly", response_class=RedirectResponse)
42+
async def redirect_to_monthly_payment(uid: str = ""):
43+
"""
44+
Redirects to Stripe payment for monthly plan
45+
"""
46+
try:
47+
if not uid or uid.strip() == "":
48+
logger.warning("Monthly payment redirect attempted without UID")
49+
return RedirectResponse(
50+
url="/subscription?error=missing_uid",
51+
status_code=302
52+
)
53+
54+
logger.info(f"Redirecting to monthly payment with UID: {uid}")
55+
56+
# Encode the UID for URL safety
57+
encoded_uid = quote(uid.strip())
58+
59+
# Monthly plan Stripe URL
60+
stripe_url = f"https://buy.stripe.com/bJedR1bG4bpcbwiahI6wE1G?client_reference_id={encoded_uid}"
61+
return RedirectResponse(
62+
url=stripe_url,
63+
status_code=302
64+
)
65+
except Exception as e:
66+
logger.error(f"Error in monthly payment redirect: {str(e)}")
67+
return RedirectResponse(
68+
url="/subscription?error=redirect_failed",
69+
status_code=302
70+
)
71+
72+
@router.get("/redirect/annual", response_class=RedirectResponse)
73+
async def redirect_to_annual_payment(uid: str = ""):
74+
"""
75+
Redirects to Stripe payment for annual plan
76+
"""
77+
try:
78+
if not uid or uid.strip() == "":
79+
logger.warning("Annual payment redirect attempted without UID")
80+
return RedirectResponse(
81+
url="/subscription?error=missing_uid",
82+
status_code=302
83+
)
84+
85+
logger.info(f"Redirecting to annual payment with UID: {uid}")
86+
87+
# Encode the UID for URL safety
88+
encoded_uid = quote(uid.strip())
89+
90+
# Annual plan Stripe URL (using same URL for demo, you can change this)
91+
stripe_url = f"https://buy.stripe.com/bJedR1bG4bpcbwiahI6wE1G?client_reference_id={encoded_uid}"
92+
return RedirectResponse(
93+
url=stripe_url,
94+
status_code=302
95+
)
96+
except Exception as e:
97+
logger.error(f"Error in annual payment redirect: {str(e)}")
98+
return RedirectResponse(
99+
url="/subscription?error=redirect_failed",
100+
status_code=302
101+
)
102+
103+
@router.get("/stats", response_class=JSONResponse)
104+
async def get_subscription_stats(request: Request):
105+
"""
106+
Returns subscription statistics
107+
Admin-only endpoint
108+
"""
109+
return JSONResponse({
110+
"status": "success",
111+
"timestamp": datetime.now().isoformat(),
112+
"message": "Subscription stats endpoint is working"
113+
})

0 commit comments

Comments
 (0)