Skip to content

Commit ae440c0

Browse files
Only consider US for apple external billing
1 parent 5cea5b1 commit ae440c0

34 files changed

Lines changed: 6091 additions & 26 deletions

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,12 @@ frontend/*.local
6565
*.tsbuildinfo
6666

6767
**/.claude/settings.local.json
68+
69+
# Rust build directories
70+
target/
71+
**/target/
72+
plugins/*/target/
73+
74+
# iOS build artifacts
75+
frontend/src-tauri/gen/apple/build/
76+
frontend/src-tauri/gen/apple/DerivedData/

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
};
1717

1818
# Use specific rust version required by Tauri
19-
rustToolchain = pkgs.rust-bin.stable."1.78.0".default.override {
19+
rustToolchain = pkgs.rust-bin.stable."1.81.0".default.override {
2020
extensions = [ "rust-src" ];
2121
};
2222
in

frontend/src-tauri/Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src-tauri/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ tauri-plugin-os = "2"
3232
tauri-plugin-sign-in-with-apple = "1.0.2"
3333
tokio = { version = "1.0", features = ["time"] }
3434
once_cell = "1.18.0"
35+
store = { path = "../../plugins/store", features = [] }
36+
37+
[target.'cfg(target_os = "ios")'.dependencies.store]
38+
path = "../../plugins/store"
39+
features = ["mobile"]

frontend/src-tauri/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use tauri::Emitter;
22
use tauri_plugin_deep_link::DeepLinkExt;
33
use tauri_plugin_opener;
4+
5+
#[cfg(all(not(desktop), target_os = "ios"))]
6+
use store;
7+
8+
#[cfg(all(not(desktop), target_os = "ios"))]
49
use tauri_plugin_sign_in_with_apple;
510

611
// This handles incoming deep links
@@ -198,7 +203,9 @@ pub fn run() {
198203
// Only add the Apple Sign In plugin on iOS
199204
#[cfg(all(not(desktop), target_os = "ios"))]
200205
{
201-
builder = builder.plugin(tauri_plugin_sign_in_with_apple::init());
206+
builder = builder
207+
.plugin(tauri_plugin_sign_in_with_apple::init())
208+
.plugin(store::init());
202209
}
203210

204211
#[cfg(not(desktop))]

frontend/src/components/Marketing.tsx

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ function PricingTier({
8888
ctaText,
8989
popular = false,
9090
productId = "", // Add productId parameter
91-
isIOS = false // Add iOS detection parameter
91+
isIOS = false, // Add iOS detection parameter
92+
externalBillingAllowed = true // Add external billing parameter with default to true
9293
}: {
9394
name: string;
9495
price: string;
@@ -98,6 +99,7 @@ function PricingTier({
9899
popular?: boolean;
99100
productId?: string; // Add type for productId
100101
isIOS?: boolean; // Add type for iOS detection
102+
externalBillingAllowed?: boolean; // Add type for external billing
101103
}) {
102104
const isTeamPlan = name.toLowerCase().includes("team");
103105
const isFreeplan = name.toLowerCase().includes("free");
@@ -133,19 +135,49 @@ function PricingTier({
133135
</div>
134136
))}
135137
</div>
136-
{/* For iOS devices, disable paid plans with "Coming Soon" text */}
138+
{/* For iOS devices, check if external billing is allowed */}
137139
{isIOS && !isFreeplan ? (
138-
<button
139-
disabled={true}
140-
className="mt-auto py-3 px-6 rounded-lg text-center font-medium transition-all duration-300
141-
dark:bg-white/90 dark:text-black dark:hover:bg-[hsl(var(--purple))]/80 dark:hover:text-[hsl(var(--foreground))] dark:active:bg-white/80
142-
bg-background text-foreground hover:bg-[hsl(var(--purple))] hover:text-[hsl(var(--foreground))] active:bg-background/80
143-
border border-[hsl(var(--purple))]/30 hover:border-[hsl(var(--purple))]
144-
shadow-[0_0_15px_rgba(var(--purple-rgb),0.2)] hover:shadow-[0_0_25px_rgba(var(--purple-rgb),0.3)]
145-
opacity-50 cursor-not-allowed"
146-
>
147-
Coming Soon
148-
</button>
140+
externalBillingAllowed ? (
141+
// If external billing is allowed, handle product selection normally
142+
productId ? (
143+
<button
144+
onClick={() => {
145+
window.location.href = `/signup?next=/pricing&selected_plan=${productId}`;
146+
}}
147+
className="mt-auto py-3 px-6 rounded-lg text-center font-medium transition-all duration-300
148+
dark:bg-white/90 dark:text-black dark:hover:bg-[hsl(var(--purple))]/80 dark:hover:text-[hsl(var(--foreground))] dark:active:bg-white/80
149+
bg-background text-foreground hover:bg-[hsl(var(--purple))] hover:text-[hsl(var(--foreground))] active:bg-background/80
150+
border border-[hsl(var(--purple))]/30 hover:border-[hsl(var(--purple))]
151+
shadow-[0_0_15px_rgba(var(--purple-rgb),0.2)] hover:shadow-[0_0_25px_rgba(var(--purple-rgb),0.3)]"
152+
>
153+
{ctaText}
154+
</button>
155+
) : (
156+
<Link
157+
to="/signup"
158+
className="mt-auto py-3 px-6 rounded-lg text-center font-medium transition-all duration-300
159+
dark:bg-white/90 dark:text-black dark:hover:bg-[hsl(var(--purple))]/80 dark:hover:text-[hsl(var(--foreground))] dark:active:bg-white/80
160+
bg-background text-foreground hover:bg-[hsl(var(--purple))] hover:text-[hsl(var(--foreground))] active:bg-background/80
161+
border border-[hsl(var(--purple))]/30 hover:border-[hsl(var(--purple))]
162+
shadow-[0_0_15px_rgba(var(--purple-rgb),0.2)] hover:shadow-[0_0_25px_rgba(var(--purple-rgb),0.3)]"
163+
>
164+
{ctaText}
165+
</Link>
166+
)
167+
) : (
168+
// If external billing is not allowed, show "Coming Soon" text
169+
<button
170+
disabled={true}
171+
className="mt-auto py-3 px-6 rounded-lg text-center font-medium transition-all duration-300
172+
dark:bg-white/90 dark:text-black dark:hover:bg-[hsl(var(--purple))]/80 dark:hover:text-[hsl(var(--foreground))] dark:active:bg-white/80
173+
bg-background text-foreground hover:bg-[hsl(var(--purple))] hover:text-[hsl(var(--foreground))] active:bg-background/80
174+
border border-[hsl(var(--purple))]/30 hover:border-[hsl(var(--purple))]
175+
shadow-[0_0_15px_rgba(var(--purple-rgb),0.2)] hover:shadow-[0_0_25px_rgba(var(--purple-rgb),0.3)]
176+
opacity-50 cursor-not-allowed"
177+
>
178+
Coming Soon
179+
</button>
180+
)
149181
) : isTeamPlan ? (
150182
// For team plans, add "Contact Us" button that opens email
151183
<button
@@ -195,8 +227,9 @@ function PricingTier({
195227

196228
export function Marketing() {
197229
const [isIOS, setIsIOS] = useState(false);
230+
const [externalBillingAllowed, setExternalBillingAllowed] = useState(true);
198231

199-
// Check if the app is running on iOS
232+
// Check if the app is running on iOS and check if external billing is allowed
200233
useEffect(() => {
201234
const checkPlatform = async () => {
202235
try {
@@ -208,7 +241,22 @@ export function Marketing() {
208241
if (isTauriEnv) {
209242
// Only check platform type if we're in a Tauri environment
210243
const platform = await type();
211-
setIsIOS(platform === "ios");
244+
const isIosDevice = platform === "ios";
245+
setIsIOS(isIosDevice);
246+
247+
// If we're on iOS, check if external billing is allowed
248+
if (isIosDevice) {
249+
try {
250+
// Import dynamically to prevent issues on non-Tauri environments
251+
const { allowExternalBilling } = await import("@/utils/region-gate");
252+
const allowed = await allowExternalBilling();
253+
setExternalBillingAllowed(allowed);
254+
console.log("External billing allowed (Marketing):", allowed);
255+
} catch (err) {
256+
console.error("Error checking store region:", err);
257+
setExternalBillingAllowed(false); // Default to false if there's an error
258+
}
259+
}
212260
} else {
213261
setIsIOS(false);
214262
}
@@ -661,6 +709,7 @@ export function Marketing() {
661709
ctaText="Start Chatting"
662710
productId={getProductId("Starter")}
663711
isIOS={isIOS}
712+
externalBillingAllowed={externalBillingAllowed}
664713
/>
665714
<PricingTier
666715
name="Pro"
@@ -676,6 +725,7 @@ export function Marketing() {
676725
popular={true}
677726
productId={getProductId("Pro")}
678727
isIOS={isIOS}
728+
externalBillingAllowed={externalBillingAllowed}
679729
/>
680730
<PricingTier
681731
name="Team"
@@ -690,6 +740,7 @@ export function Marketing() {
690740
ctaText="Contact Us"
691741
productId={getProductId("Team")}
692742
isIOS={isIOS}
743+
externalBillingAllowed={externalBillingAllowed}
693744
/>
694745
</div>
695746
</div>

frontend/src/routes/auth.$provider.callback.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function OAuthCallback() {
120120
};
121121

122122
processCallback();
123-
}, [handleGitHubCallback, handleGoogleCallback, navigate, provider]);
123+
}, [handleGitHubCallback, handleGoogleCallback, navigate, provider, handleSuccessfulAuth]);
124124

125125
// If this is a Tauri app auth flow (desktop or mobile), show a different UI
126126
if (localStorage.getItem("redirect-to-native") === "true") {

frontend/src/routes/pricing.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ function PricingPage() {
148148
const isLoggedIn = !!os.auth.user;
149149
const { selected_plan } = Route.useSearch();
150150

151-
// Check if the app is running on iOS
151+
// Check if the app is running on iOS and check if external billing is allowed
152+
const [externalBillingAllowed, setExternalBillingAllowed] = useState(true);
153+
152154
useEffect(() => {
153155
const checkPlatform = async () => {
154156
try {
@@ -160,7 +162,22 @@ function PricingPage() {
160162
if (isTauriEnv) {
161163
// Only check platform type if we're in a Tauri environment
162164
const platform = await type();
163-
setIsIOS(platform === "ios");
165+
const isIosDevice = platform === "ios";
166+
setIsIOS(isIosDevice);
167+
168+
// If we're on iOS, check if external billing is allowed
169+
if (isIosDevice) {
170+
try {
171+
// Import dynamically to prevent issues on non-Tauri environments
172+
const { allowExternalBilling } = await import("@/utils/region-gate");
173+
const allowed = await allowExternalBilling();
174+
setExternalBillingAllowed(allowed);
175+
console.log("External billing allowed:", allowed);
176+
} catch (err) {
177+
console.error("Error checking store region:", err);
178+
setExternalBillingAllowed(false); // Default to false if there's an error
179+
}
180+
}
164181
} else {
165182
setIsIOS(false);
166183
}
@@ -185,12 +202,15 @@ function PricingPage() {
185202
enabled: isLoggedIn
186203
});
187204

188-
// Auto-enable Bitcoin toggle for Zaprite users (except on iOS)
205+
// Auto-enable Bitcoin toggle for Zaprite users (except on iOS or if external billing is not allowed)
189206
useEffect(() => {
190-
if (freshBillingStatus?.payment_provider === "zaprite" && !isIOS) {
207+
if (
208+
freshBillingStatus?.payment_provider === "zaprite" &&
209+
(!isIOS || (isIOS && externalBillingAllowed))
210+
) {
191211
setUseBitcoin(true);
192212
}
193-
}, [freshBillingStatus?.payment_provider, isIOS]);
213+
}, [freshBillingStatus?.payment_provider, isIOS, externalBillingAllowed]);
194214

195215
// Always try to fetch portal URL if logged in
196216
const { data: portalUrl } = useQuery({
@@ -245,6 +265,11 @@ function PricingPage() {
245265
const { success, canceled } = Route.useSearch();
246266

247267
const getButtonText = (product: Product) => {
268+
// For iOS with paid plans (not Free) in non-US regions, show "Coming Soon"
269+
if (isIOS && !product.name.toLowerCase().includes("free") && !externalBillingAllowed) {
270+
return "Coming Soon";
271+
}
272+
248273
if (loadingProductId === product.id) {
249274
return (
250275
<>
@@ -422,6 +447,13 @@ function PricingPage() {
422447

423448
const handleButtonClick = useCallback(
424449
(product: Product) => {
450+
// This check is now redundant since we disable the button and show "Coming Soon"
451+
// But keep it as a safeguard
452+
if (isIOS && !product.name.toLowerCase().includes("free") && !externalBillingAllowed) {
453+
// Don't allow any action on paid plans if on iOS and external billing not allowed
454+
return;
455+
}
456+
425457
if (!isLoggedIn) {
426458
const targetPlanName = product.name.toLowerCase();
427459
const isTeamPlan = targetPlanName.includes("team");
@@ -500,7 +532,8 @@ function PricingPage() {
500532
navigate,
501533
portalUrl,
502534
newHandleSubscribe,
503-
isIOS
535+
isIOS,
536+
externalBillingAllowed
504537
]
505538
);
506539

@@ -646,7 +679,7 @@ function PricingPage() {
646679
</div>
647680
)}
648681

649-
{!isIOS && (
682+
{(!isIOS || (isIOS && externalBillingAllowed)) && (
650683
<div className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-center">
651684
<div className="inline-flex items-center gap-4 px-6 py-2.5 rounded-full bg-[hsl(var(--marketing-card))]/50 backdrop-blur-sm border border-[hsl(var(--marketing-card-border))]">
652685
<div className="flex items-center gap-2 text-[hsl(var(--bitcoin))] text-base font-light">
@@ -778,7 +811,9 @@ function PricingPage() {
778811
<button
779812
onClick={() => handleButtonClick(product)}
780813
disabled={
781-
loadingProductId === product.id || (useBitcoin && product.name === "Team")
814+
loadingProductId === product.id ||
815+
(useBitcoin && product.name === "Team") ||
816+
(isIOS && !product.name.toLowerCase().includes("free") && !externalBillingAllowed)
782817
}
783818
className={`w-full
784819
dark:bg-white/90 dark:text-black dark:hover:bg-[hsl(var(--purple))]/80 dark:hover:text-[hsl(var(--foreground))] dark:active:bg-white/80

frontend/src/utils/region-gate.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { invoke } from "@tauri-apps/api/core";
2+
3+
const US_CODES = ["USA", "ASM", "GUM", "PRI", "VIR", "MNP", "UMI"]; // US and territories
4+
5+
/**
6+
* Checks if external billing is allowed based on the App Store region.
7+
* Returns true for US regions, false for all others or on errors.
8+
*/
9+
export async function allowExternalBilling(): Promise<boolean> {
10+
try {
11+
const code = await invoke<string>("plugin:store|get_region");
12+
return US_CODES.includes(code);
13+
} catch (error) {
14+
console.error("Error checking store region:", error);
15+
return false; // Safest fallback is to not allow external billing
16+
}
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/config/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc
10+
Package.resolved

0 commit comments

Comments
 (0)