Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ react {
// skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]

debuggableVariants = ["debug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,34 @@ import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative

class MainApplication : Application(), ReactApplication {

override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
},
)
}
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {

override fun getPackages(): List<ReactPackage> =
PackageList(this).packages

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)

override fun onCreate() {
super.onCreate()
loadReactNative(this)
}
}

12 changes: 12 additions & 0 deletions apps/mobile/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export const API_BASE_URL: string = __DEV__
? `http://${DEV_HOST}:3000`
: 'https://api.devcard.dev';

// OAuth must use the same host for start and callback so browser cookies/state match.
// Android reaches the host machine via `adb reverse tcp:3000 tcp:3000`.
export const AUTH_BASE_URL: string = __DEV__
? 'http://localhost:3000'
: 'https://api.devcard.dev';

export const APP_URL: string = __DEV__
? 'http://localhost:5173'
: 'https://devcard.dev';
Expand All @@ -20,3 +26,9 @@ export const APP_URL: string = __DEV__
export const DEEP_LINK_SCHEME = 'devcard';

export const OAUTH_REDIRECT_URI = `${DEEP_LINK_SCHEME}://oauth/callback`;

// Backend OAuth placeholders. Replace these once backend routes are finalized.
export const BACKEND_AUTH_ROUTES = {
github: '/auth/github',
google: '/auth/google',
} as const;
36 changes: 36 additions & 0 deletions apps/mobile/src/services/backendAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Linking } from 'react-native';
import { AUTH_BASE_URL, BACKEND_AUTH_ROUTES, OAUTH_REDIRECT_URI } from '../config';

type AuthProvider = keyof typeof BACKEND_AUTH_ROUTES;

export function buildBackendAuthUrl(provider: AuthProvider) {
const route = BACKEND_AUTH_ROUTES[provider];
const params = new URLSearchParams({
client: 'mobile',
provider,
state: `mobile_${provider}`,
mobile_redirect_uri: OAUTH_REDIRECT_URI,
});

return `${AUTH_BASE_URL}${route}?${params.toString()}`;
}

export async function startBackendOAuth(provider: AuthProvider) {
const url = buildBackendAuthUrl(provider);
const canOpen = await Linking.canOpenURL(url);

if (!canOpen) {
throw new Error(`Cannot open auth URL for ${provider}.`);
}

await Linking.openURL(url);
}

export function getJwtFromCallbackUrl(url: string) {
if (!url.startsWith(OAUTH_REDIRECT_URI)) return null;

const queryString = url.includes('#') ? url.split('#')[1] : url.split('?')[1] || '';
const params = new URLSearchParams(queryString);

return params.get('token') || params.get('jwt') || params.get('access_token');
}
2 changes: 1 addition & 1 deletion packages/shared/src/__tests__/cards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ describe('diffCardPlatforms', () => {
expect(diff.removed).toEqual([]);
expect(diff.unchanged).toEqual(['github']);
});
});
});
6 changes: 0 additions & 6 deletions packages/shared/src/__tests__/platforms-url.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { describe, it, expect } from 'vitest';
import { getProfileUrl, getWebViewUrl, getDeepLinkUrl } from '../platforms';

// ─── getProfileUrl Tests ───

describe('getProfileUrl', () => {
it('should return the correct GitHub profile URL', () => {
expect(getProfileUrl('github', 'octocat')).toBe('https://github.com/octocat');
Expand All @@ -21,8 +19,6 @@ describe('getProfileUrl', () => {
});
});

// ─── getWebViewUrl Tests ───

describe('getWebViewUrl', () => {
it('should return the correct LinkedIn webview URL', () => {
expect(getWebViewUrl('linkedin', 'john')).toBe('https://www.linkedin.com/in/john');
Expand All @@ -41,8 +37,6 @@ describe('getWebViewUrl', () => {
});
});

// ─── getDeepLinkUrl Tests ───

describe('getDeepLinkUrl', () => {
it('should return the correct Twitter deep link URL', () => {
expect(getDeepLinkUrl('twitter', 'john')).toBe('twitter://user?screen_name=john');
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ export function diffCardPlatforms(
removed: oldCard.filter(p => !newSet.has(p)),
unchanged: oldCard.filter(p => newSet.has(p)),
};
}
}
21 changes: 5 additions & 16 deletions packages/shared/src/platforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
getDeepLinkUrl,
} from './platforms';

// ─── StackOverflow Platform Tests ───

describe('stackoverflow platform', () => {
it('should exist in PLATFORMS registry', () => {
expect(PLATFORMS.stackoverflow).toBeDefined();
Expand Down Expand Up @@ -44,9 +42,7 @@ describe('stackoverflow platform', () => {
});
});

// ─── getProfileUrl Tests for StackOverflow ───

describe('getProfileUrl – stackoverflow', () => {
describe('getProfileUrl - stackoverflow', () => {
it('should generate correct URL with user ID and display name', () => {
const url = getProfileUrl('stackoverflow', '1234/user');
expect(url).toBe('https://stackoverflow.com/users/1234/user');
Expand All @@ -63,9 +59,7 @@ describe('getProfileUrl – stackoverflow', () => {
});
});

// ─── getWebViewUrl / getDeepLinkUrl for StackOverflow ───

describe('getWebViewUrl / getDeepLinkUrl – stackoverflow', () => {
describe('getWebViewUrl / getDeepLinkUrl - stackoverflow', () => {
it('should return null for webViewUrl (not supported)', () => {
expect(getWebViewUrl('stackoverflow', '1234/user')).toBeNull();
});
Expand All @@ -75,15 +69,12 @@ describe('getWebViewUrl / getDeepLinkUrl – stackoverflow', () => {
});
});

// ─── validationRegex Tests ───

describe('validationRegex logic', () => {
it('should correctly validate github usernames', () => {
const regex = PLATFORMS.github.validationRegex!;
expect(regex.test('valid-user')).toBe(true);
expect(regex.test('a')).toBe(true);
expect(regex.test('user123')).toBe(true);
// Invalid
expect(regex.test('-invalid')).toBe(false);
expect(regex.test('invalid-')).toBe(false);
expect(regex.test('in--valid')).toBe(false);
Expand All @@ -94,18 +85,16 @@ describe('validationRegex logic', () => {
const regex = PLATFORMS.linkedin.validationRegex!;
expect(regex.test('valid-user')).toBe(true);
expect(regex.test('user123')).toBe(true);
// Invalid
expect(regex.test('ab')).toBe(false); // Too short
expect(regex.test('ab')).toBe(false);
expect(regex.test('user name')).toBe(false);
});

it('should correctly validate twitter usernames', () => {
const regex = PLATFORMS.twitter.validationRegex!;
expect(regex.test('valid_user')).toBe(true);
expect(regex.test('user123')).toBe(true);
// Invalid
expect(regex.test('user-name')).toBe(false); // Hyphens not allowed
expect(regex.test('this_is_a_very_long_name_indeed')).toBe(false); // Too long
expect(regex.test('user-name')).toBe(false);
expect(regex.test('this_is_a_very_long_name_indeed')).toBe(false);
expect(regex.test('user name')).toBe(false);
});
});
22 changes: 11 additions & 11 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// ─── User Types ───

export interface User {
id: string;
email: string;
Expand All @@ -24,8 +22,6 @@ export interface UpdateProfilePayload {
accentColor?: string;
}

// ─── Platform Link Types ───

export interface PlatformLink {
id: string;
platform: string;
Expand All @@ -44,27 +40,35 @@ export interface ReorderLinksPayload {
links: Array<{ id: string; displayOrder: number }>;
}

// ─── Card Types ───
export type CardVisibility = 'PUBLIC' | 'UNLISTED' | 'PRIVATE';

export interface Card {
id: string;
title: string;
description?: string | null;
slug?: string;
visibility?: CardVisibility;
qrEnabled?: boolean;
viewCount?: number;
isDefault: boolean;
links: PlatformLink[];
}

export interface CreateCardPayload {
title: string;
linkIds: string[];
description?: string;
visibility?: CardVisibility;
}

export interface UpdateCardPayload {
title?: string;
linkIds?: string[];
description?: string;
visibility?: CardVisibility;
qrEnabled?: boolean;
}

// ─── Public Profile Types ───

export interface PublicProfile {
username: string;
displayName: string;
Expand Down Expand Up @@ -92,8 +96,6 @@ export interface PublicCard {
links: PlatformLink[];
}

// ─── Follow Engine Types ───

export type FollowStatus = 'idle' | 'loading' | 'success' | 'error';

export interface FollowResult {
Expand All @@ -103,8 +105,6 @@ export interface FollowResult {
message?: string;
}

// ─── Auth Types ───

export interface AuthResponse {
token: string;
user: User;
Expand Down
Loading