mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
open pricing window to stripe
This commit is contained in:
1 parent
859897bedb
commit
474904ddd7
21 files changed
+287
-59
No files matched your search
@@ -7,4 +7,5 @@ pub mod jobs;
|
||||
pub mod media_files;
|
||||
pub mod prompts;
|
||||
pub mod recipes;
|
||||
pub mod stripe_artcraft;
|
||||
pub mod utils;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{StripeArtcraftCreateCheckoutSessionRequest, StripeArtcraftCreateCheckoutSessionResponse, CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH};
|
||||
|
||||
pub async fn create_subscription_checkout(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: StripeArtcraftCreateCheckoutSessionRequest,
|
||||
) -> Result<StripeArtcraftCreateCheckoutSessionResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod create_subscription_checkout;
|
||||
@@ -45,6 +45,7 @@ use crate::services::sora::commands::sora_logout_command::sora_logout_command;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread;
|
||||
use crate::services::storyteller::commands::storyteller_open_billing_command::storyteller_open_billing_command;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use log::error;
|
||||
|
||||
@@ -170,16 +171,17 @@ pub fn run() {
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
load_without_cors_command,
|
||||
open_sora_login_command,
|
||||
midjourney_clear_credentials_command,
|
||||
midjourney_get_credential_info_command,
|
||||
midjourney_open_login_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
set_provider_order_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
sora_logout_command,
|
||||
storyteller_open_billing_command,
|
||||
update_app_preferences_command,
|
||||
])
|
||||
.run(tauri::generate_context!("tauri.conf.json"))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod storyteller_open_billing_command;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::midjourney::windows::open_midjourney_login_window::open_midjourney_login_window;
|
||||
use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::services::storyteller::windows::open_storyteller_billing_window::open_storyteller_billing_window;
|
||||
use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{PlanBillingCadence, PlanName};
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::Deserialize;
|
||||
use tauri::{AppHandle, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct StorytellerOpenBillingCommand {
|
||||
pub plan: Option<PlanName>,
|
||||
pub cadence: Option<PlanBillingCadence>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn storyteller_open_billing_command(
|
||||
app: AppHandle,
|
||||
request: StorytellerOpenBillingCommand,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
) -> Result<String, String> {
|
||||
info!("storyteller_open_billing_command called");
|
||||
|
||||
do_open_billing(
|
||||
&app,
|
||||
&app_data_root,
|
||||
&app_env_configs,
|
||||
&storyteller_creds_manager,
|
||||
request.plan,
|
||||
request.cadence,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error opening login: {:?}", err);
|
||||
format!("Error opening login: {:?}", err)
|
||||
})?;
|
||||
|
||||
Ok("result".to_string())
|
||||
}
|
||||
|
||||
async fn do_open_billing(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
plan: Option<PlanName>,
|
||||
cadence: Option<PlanBillingCadence>,
|
||||
) -> AnyhowResult<()> {
|
||||
info!("Building billing window...");
|
||||
|
||||
open_storyteller_billing_window(
|
||||
app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
storyteller_creds_manager,
|
||||
plan,
|
||||
cadence,
|
||||
).await?;
|
||||
|
||||
info!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod threads;
|
||||
pub mod windows;
|
||||
pub mod commands;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod open_storyteller_billing_window;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{PlanBillingCadence, PlanName, StripeArtcraftCreateCheckoutSessionRequest};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use reqwest::Url;
|
||||
use storyteller_client::stripe_artcraft::create_subscription_checkout::create_subscription_checkout;
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
const BILLING_WINDOW_NAME: &str = "artcraft_billing_window";
|
||||
|
||||
pub async fn open_storyteller_billing_window(
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
plan: Option<PlanName>,
|
||||
cadence: Option<PlanBillingCadence>,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
if app.get_window(BILLING_WINDOW_NAME).is_some() {
|
||||
return Err(anyhow!("Billing window already open"));
|
||||
}
|
||||
|
||||
let url = do_get_url(
|
||||
app,
|
||||
app_env_configs,
|
||||
app_data_root,
|
||||
storyteller_creds_manager,
|
||||
plan,
|
||||
cadence,
|
||||
).await?;
|
||||
|
||||
do_open_window(app, app_data_root, url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_get_url(
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
plan: Option<PlanName>,
|
||||
cadence: Option<PlanBillingCadence>,
|
||||
) -> AnyhowResult<Url> {
|
||||
|
||||
info!("Checkout session requires session...");
|
||||
|
||||
let creds = storyteller_creds_manager.get_credentials_required()?;
|
||||
|
||||
info!("Creating checkout session...");
|
||||
|
||||
let request = StripeArtcraftCreateCheckoutSessionRequest {
|
||||
plan,
|
||||
cadence,
|
||||
};
|
||||
|
||||
let result = create_subscription_checkout(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await?;
|
||||
|
||||
info!("Checkout session created...");
|
||||
|
||||
let url = Url::parse(&result.stripe_checkout_redirect_url)?;
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
async fn do_open_window(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
url: Url,
|
||||
) -> AnyhowResult<()> {
|
||||
let url = WebviewUrl::External(url);
|
||||
|
||||
info!("Opening checkout window...");
|
||||
|
||||
// TODO(bt,2025-09-01): We probably need to open this in a real browser to get autocomplete.
|
||||
let window = WebviewWindowBuilder::new(app, BILLING_WINDOW_NAME, url)
|
||||
//.user_agent(openai_sora_client::credentials::USER_AGENT)
|
||||
.always_on_top(false)
|
||||
.title("Sign up for ArtCraft")
|
||||
.center()
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.closable(true)
|
||||
.min_inner_size(1000.0, 800.0)
|
||||
.focused(true)
|
||||
.devtools(true)
|
||||
.build()?;
|
||||
|
||||
let webview = window.get_webview(BILLING_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow!("no webview found"))?;
|
||||
|
||||
//webview.navigate(MIDJOURNEY_HOMEPAGE_URL.clone())?;
|
||||
|
||||
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
|
||||
//tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
//webview.navigate(MIDJOURNEY_LOGIN_URL.clone())?;
|
||||
|
||||
let app_handle = app.clone();
|
||||
let app_data_root = app_data_root.clone();
|
||||
|
||||
//let _ = tauri::async_runtime::spawn(async move {
|
||||
// midjourney_login_window_thread(app_handle, app_data_root).await;
|
||||
//});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,4 +2,5 @@ pub mod common;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
pub mod media_file;
|
||||
pub mod prompts;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/create_subscription";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct StripeArtcraftCreateCheckoutSessionRequest {
|
||||
/// The (non-Stripe) internal identifier for the product or subscription.
|
||||
/// This will be translated into a Stripe identifier.
|
||||
pub plan: Option<PlanName>,
|
||||
|
||||
pub cadence: Option<PlanBillingCadence>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
|
||||
pub enum PlanName {
|
||||
#[serde(rename = "basic")]
|
||||
Basic,
|
||||
#[serde(rename = "pro")]
|
||||
Pro,
|
||||
#[serde(rename = "max")]
|
||||
Max,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
|
||||
pub enum PlanBillingCadence {
|
||||
#[serde(rename = "monthly")]
|
||||
Monthly,
|
||||
|
||||
#[serde(rename = "yearly")]
|
||||
Yearly,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct StripeArtcraftCreateCheckoutSessionResponse {
|
||||
pub success: bool,
|
||||
pub stripe_checkout_redirect_url: String,
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod create_subscription_checkout;
|
||||
@@ -2,5 +2,3 @@
|
||||
pub const STRIPE_ARTCRAFT_METADATA_USER_TOKEN : &str = "user_token";
|
||||
pub const STRIPE_ARTCRAFT_METADATA_USERNAME : &str = "username";
|
||||
pub const STRIPE_ARTCRAFT_METADATA_EMAIL : &str = "email";
|
||||
|
||||
pub const STRIPE_ARTCRAFT_METADATA_TOLT_REFERRAL: &str = "tolt_referral";
|
||||
+1
-1
@@ -1 +1 @@
|
||||
pub mod stripe_artcraft_create_checkout_session_handler;
|
||||
pub mod stripe_artcraft_create_subscription_checkout_session_handler;
|
||||
+2
-43
@@ -1,4 +1,4 @@
|
||||
use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_TOLT_REFERRAL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN};
|
||||
use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN};
|
||||
use crate::configs::stripe_artcraft_product_info_list::{ARTCRAFT_BASIC_SANDBOX, ARTCRAFT_MAX_SANDBOX, ARTCRAFT_PRO_SANDBOX};
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -14,44 +14,7 @@ use std::sync::Arc;
|
||||
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
|
||||
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct StripeArtcraftCreateCheckoutSessionRequest {
|
||||
/// The (non-Stripe) internal identifier for the product or subscription.
|
||||
/// This will be translated into a Stripe identifier.
|
||||
plan: Option<PlanName>,
|
||||
|
||||
cadence: Option<PlanBillingCadence>,
|
||||
|
||||
/// Optional Tolt referral code
|
||||
/// See: https://help.tolt.io/en/articles/6843411-how-to-set-up-stripe-with-tolt
|
||||
maybe_tolt_referral: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub enum PlanName {
|
||||
#[serde(rename = "basic")]
|
||||
Basic,
|
||||
#[serde(rename = "pro")]
|
||||
Pro,
|
||||
#[serde(rename = "max")]
|
||||
Max,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub enum PlanBillingCadence {
|
||||
#[serde(rename = "monthly")]
|
||||
Monthly,
|
||||
|
||||
#[serde(rename = "yearly")]
|
||||
Yearly,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct StripeArtcraftCreateCheckoutSessionResponse {
|
||||
pub success: bool,
|
||||
pub stripe_checkout_redirect_url: String,
|
||||
}
|
||||
use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{PlanBillingCadence, PlanName, StripeArtcraftCreateCheckoutSessionRequest, StripeArtcraftCreateCheckoutSessionResponse};
|
||||
|
||||
/// Create a Stripe Checkout session and return the redirect URL in Json.
|
||||
#[utoipa::path(
|
||||
@@ -156,10 +119,6 @@ pub async fn stripe_artcraft_create_checkout_session_handler(
|
||||
metadata.insert(STRIPE_ARTCRAFT_METADATA_EMAIL.to_string(), user_email.to_string());
|
||||
}
|
||||
|
||||
if let Some(tolt_referral) = request.maybe_tolt_referral.as_deref() {
|
||||
metadata.insert(STRIPE_ARTCRAFT_METADATA_TOLT_REFERRAL.to_string(), tolt_referral.to_string());
|
||||
}
|
||||
|
||||
// NB: This metadata attaches to Stripe's Checkout Session object.
|
||||
// This does not attach to the subscription or payment intent, which have their own metadata
|
||||
// objects. (TODO: Confirm this.)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::http_server::endpoints::stripe_artcraft::checkout::stripe_artcraft_create_checkout_session_handler::stripe_artcraft_create_checkout_session_handler;
|
||||
use crate::http_server::endpoints::stripe_artcraft::checkout::stripe_artcraft_create_subscription_checkout_session_handler::stripe_artcraft_create_checkout_session_handler;
|
||||
use crate::http_server::endpoints::stripe_artcraft::webhook::stripe_artcraft_webhook_handler::stripe_artcraft_webhook_handler;
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.4.0"
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,10 @@ export const pricingConfig = {
|
||||
colorScheme: "dark" as const,
|
||||
},
|
||||
{
|
||||
id: "plus",
|
||||
name: "Plus",
|
||||
//id: "plus",
|
||||
//name: "Plus",
|
||||
id: "basic",
|
||||
name: "Basic",
|
||||
monthlyPrice: 8,
|
||||
yearlyPrice: 96,
|
||||
originalMonthlyPrice: 10,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { pricingConfig, PricingTier } from "./pricing-config";
|
||||
import { usePricingModalStore } from "./pricing-modal-store";
|
||||
import { TabSelector } from "@storyteller/ui-tab-selector";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const billingTabs = [
|
||||
{ id: "yearly", label: "Yearly" },
|
||||
@@ -32,14 +33,21 @@ export function PricingModal({
|
||||
const handleUpgrade = async (tierId: string) => {
|
||||
// TODO: Implement Stripe checkout
|
||||
const tier = pricingConfig.tiers.find((t) => t.id === tierId);
|
||||
const priceId = isYearly ? tier?.yearlyPriceId : tier?.monthlyPriceId;
|
||||
//const priceId = isYearly ? tier?.yearlyPriceId : tier?.monthlyPriceId;
|
||||
|
||||
console.log(
|
||||
`Upgrading to ${tierId} plan with ${
|
||||
isYearly ? "yearly" : "monthly"
|
||||
} billing`,
|
||||
{ priceId, tierId }
|
||||
);
|
||||
const planName = tier?.id;
|
||||
const cadence = isYearly? "yearly" : "monthly";
|
||||
|
||||
if (planName === "free") {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke("storyteller_open_billing_command", {
|
||||
request: {
|
||||
plan:planName,
|
||||
cadence: cadence,
|
||||
}
|
||||
});
|
||||
|
||||
// Example Stripe checkout implementation:
|
||||
// await stripe.redirectToCheckout({
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../tab-selector"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../tab-selector/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user