Skip to content

Commit

Permalink
feat: setup a basic graphql config
Browse files Browse the repository at this point in the history
  • Loading branch information
tsyirvo committed Apr 13, 2024
1 parent 916f164 commit 95bb193
Show file tree
Hide file tree
Showing 31 changed files with 8,124 additions and 168 deletions.
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
EAS_PROJECT_ID=""
APP_NAME="Dev"

API_URL=""

SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
Expand Down
2 changes: 2 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
EAS_PROJECT_ID=""
APP_NAME="RN Starter"

API_URL=""

SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
Expand Down
2 changes: 2 additions & 0 deletions .env.staging
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
EAS_PROJECT_ID=""
APP_NAME="Staging"

API_URL=""

SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
Expand Down
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@
"json"
],
"doppler.autocomplete.enable": true,
"doppler.hover.enable": true
"doppler.hover.enable": true,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
48 changes: 48 additions & 0 deletions codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
overwrite: true,
schema: 'https://graphqlzero.almansi.me/api',
documents: 'src/**/*.tsx',
generates: {
// 'src/gql/generated/types.ts': {
// plugins: ['typescript'],
// config: {
// enumsAsTypes: true,
// disableDescriptions: true,
// strictScalars: true,
// defaultScalarType: 'unknown',
// scalars: {
// Date: 'string',
// DateTime: 'string',
// },
// },
// },
'src/gql/generated/': {
preset: 'client',
},
'src/gql/generated/hooks.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-query',
],
config: {
reactQueryVersion: 5,
exposeQueryKeys: true,
exposeFetcher: true,
fetcher: '../../core/api/request#request',
},
},
'src/gql/graphql.schema.json': {
plugins: ['introspection'],
},
},
hooks: {
afterAllFileWrite: [
"eslint ./src/gql --ext .ts,.json --fix && yarn prettier --write './src/gql/**/*.ts'",
],
},
};

export default config;
2 changes: 2 additions & 0 deletions env.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const client = z.object({
VERSION: z.string(),

// ADD CLIENT ENV VARS HERE
API_URL: z.string(),
FLAGSMITH_KEY: z.string(),
MIXPANEL_TOKEN: z.string(),
SENTRY_DSN: z.string(),
Expand All @@ -62,6 +63,7 @@ const _clientEnv = {

// ADD ENV VARS HERE TOO
APP_NAME: process.env.APP_NAME,
API_URL: process.env.API_URL,
FLAGSMITH_KEY: process.env.FLAGSMITH_KEY,
MIXPANEL_TOKEN: process.env.MIXPANEL_TOKEN,
SENTRY_DSN: process.env.SENTRY_DSN,
Expand Down
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,18 @@
"prepare": "husky",
"postinstall": "husky && patch-package",
"prepack": "pinst --disable",
"postpack": "pinst --enable"
"postpack": "pinst --enable",
"codegen": "graphql-codegen --config codegen.ts"
},
"dependencies": {
"@react-native-masked-view/masked-view": "0.3.0",
"@react-navigation/native": "6.1.17",
"@react-navigation/native-stack": "6.9.26",
"@sentry/react-native": "5.19.1",
"@shopify/restyle": "2.4.2",
"@tanstack/query-async-storage-persister": "5.29.1",
"@tanstack/react-query": "5.29.2",
"@tanstack/react-query-persist-client": "5.29.2",
"dayjs": "1.11.10",
"expo": "~50.0.14",
"expo-application": "~5.8.3",
Expand All @@ -91,6 +95,8 @@
"expo-status-bar": "~1.11.1",
"expo-updates": "~0.24.12",
"flagsmith": "3.23.2",
"graphql": "16.8.1",
"graphql-request": "6.1.0",
"i18next": "23.10.1",
"lodash": "4.17.21",
"mixpanel-react-native": "2.4.0",
Expand All @@ -113,12 +119,19 @@
"zod": "3.22.4"
},
"devDependencies": {
"@0no-co/graphqlsp": "1.9.1",
"@babel/core": "7.23.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@commitlint/cli": "19.2.1",
"@commitlint/config-conventional": "19.1.0",
"@config-plugins/detox": "3.0.0",
"@graphql-codegen/cli": "5.0.2",
"@graphql-codegen/client-preset": "4.2.5",
"@graphql-codegen/introspection": "4.0.3",
"@graphql-codegen/typescript": "4.0.6",
"@graphql-codegen/typescript-operations": "4.2.0",
"@graphql-codegen/typescript-react-query": "6.1.0",
"@svgr/cli": "8.1.0",
"@testing-library/jest-native": "5.4.3",
"@testing-library/react-native": "12.4.5",
Expand Down
39 changes: 23 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ThemeProvider } from '@shopify/restyle';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import type { ErrorInfo } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { StatusBar, StyleSheet } from 'react-native';
Expand All @@ -9,6 +10,7 @@ import {
} from 'react-native-safe-area-context';
import Toast from 'react-native-toast-message';

import { persistOptions, queryClient } from '$core/api/queryClient';
import { bootstrapExternalSdks } from '$core/bootstrapExternalSdks';
import { ErrorMonitoring } from '$core/monitoring';
import { RootStack } from '$core/navigation';
Expand Down Expand Up @@ -51,26 +53,31 @@ const App = () => {
<StatusBar barStyle="light-content" />

<GestureHandlerRootView style={styles.container}>
<ErrorBoundary
FallbackComponent={FullscreenErrorBoundary}
onError={onGlobalError}
<PersistQueryClientProvider
client={queryClient}
persistOptions={persistOptions}
>
<Splashscreen>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Sandbox>
<>
<RootStack />
<ErrorBoundary
FallbackComponent={FullscreenErrorBoundary}
onError={onGlobalError}
>
<Splashscreen>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Sandbox>
<>
<RootStack />

<Toast config={toastConfig} />
<Toast config={toastConfig} />

<AppUpdateNeeded />
<AppUpdateNeeded />

<MaintenanceMode />
</>
</Sandbox>
</SafeAreaProvider>
</Splashscreen>
</ErrorBoundary>
<MaintenanceMode />
</>
</Sandbox>
</SafeAreaProvider>
</Splashscreen>
</ErrorBoundary>
</PersistQueryClientProvider>
</GestureHandlerRootView>
</ThemeProvider>
);
Expand Down
35 changes: 35 additions & 0 deletions src/core/api/queryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { QueryClient } from '@tanstack/react-query';
import type { PersistQueryClientOptions } from '@tanstack/react-query-persist-client';

import { storageKeys } from '$core/constants';
import { QueryClientStorage } from '$core/storage';

import {
GC_TIME,
STALE_TIME,
THIRTY_DAYS,
retryDelay,
} from './utils/queryClient.utils';

const asyncStoragePersister = createAsyncStoragePersister({
key: storageKeys.queryStorage.id,
storage: QueryClientStorage,
});

export const persistOptions: Omit<PersistQueryClientOptions, 'queryClient'> = {
persister: asyncStoragePersister,
buster: 'v1',
maxAge: THIRTY_DAYS,
};

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay,
staleTime: STALE_TIME, // 5 minutes
gcTime: GC_TIME, // 24 hours
refetchOnWindowFocus: false,
},
},
});
39 changes: 39 additions & 0 deletions src/core/api/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GraphQLClient } from 'graphql-request';
import type {
GraphQLClientRequestHeaders,
Variables,
} from 'graphql-request/build/esm/types';
import i18next from 'i18next';
import memoize from 'lodash/memoize';

import { config } from '$core/constants';
import { getCurrentLocale } from '$core/i18n/utils/getCurrentLocale';

import { getAppIdentifier } from './utils/request.utils';

const getClientEndpoint = (env: string) =>
`${env}?lang=${getCurrentLocale(i18next)}`;

const getQueryClient = memoize(
(env: string) => new GraphQLClient(getClientEndpoint(env)),
(...args) => args.join('_'),
);

let client: GraphQLClient | undefined;

export const request =
<TData, TVariables extends Variables>(
query: string,
variables?: TVariables,
options?: GraphQLClientRequestHeaders,
): (() => Promise<TData>) =>
async () => {
client = getQueryClient(config.apiURL);
client.setEndpoint(getClientEndpoint(config.apiURL));

if (options) client.setHeaders(options);
client.setHeader('app-id', getAppIdentifier());
client.setHeader('app-version', config.version);

return client.request(query, variables);
};
14 changes: 14 additions & 0 deletions src/core/api/utils/queryClient.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */

const ONE_SECOND = 1_000;
const MAX_RETRY_DELAY = 30_000;
const FIVE_MINUTES = 1000 * 60 * 5;
const TWENTY_FOUR_HOURS = 1000 * 60 * 60 * 24;

export const THIRTY_DAYS = 30 * TWENTY_FOUR_HOURS;

export const retryDelay = (attemptIndex: number) =>
Math.min(ONE_SECOND * 2 ** attemptIndex, MAX_RETRY_DELAY);

export const STALE_TIME = FIVE_MINUTES;
export const GC_TIME = TWENTY_FOUR_HOURS;
8 changes: 8 additions & 0 deletions src/core/api/utils/request.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Platform } from 'react-native';

import { config } from '$core/constants';

export function getAppIdentifier() {
// com.tsyirvo.rnstarter/2.0.0(777)_ios
return `${config.bundleId}/${config.version}${config.buildNumber ? `(${config.buildNumber})` : ''}_${Platform.OS}`;
}
18 changes: 13 additions & 5 deletions src/core/constants/config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
import Constants from 'expo-constants';

import { IS_IOS } from './platform';

//@ts-expect-error // We know we're passing the correct environment variables to `extra` in `app.config.ts`
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const Env: typeof import('../../../env.js').ClientEnv =
Constants.expoConfig?.extra ?? {};

const env = Env.APP_ENV;
const version = Env.VERSION;
const buildNumber = Constants.expoConfig?.ios?.buildNumber;
const iosbuildNumber = Constants.expoConfig?.ios?.buildNumber ?? '';
const androidVersionCode = Constants.expoConfig?.android?.versionCode
? Constants.expoConfig.android.versionCode.toString()
: '';
const runtimeVersion = Constants.expoConfig?.runtimeVersion;
const iosBundleIdentifier = Constants.expoConfig?.ios?.bundleIdentifier ?? '';
const androidPackageName = Constants.expoConfig?.android?.package ?? '';
const apiURL = Env.API_URL;
const sentryDsn = Env.SENTRY_DSN;
const mixpanelToken = Env.MIXPANEL_TOKEN;
const flagsmithKey = Env.FLAGSMITH_KEY;

export const config = {
defaultLocale: 'en',
supportedLocales: ['en', 'fr'],
// Config
// App config
env,
isDebug: env === 'development',
version,
buildNumber,
buildNumber: IS_IOS ? iosbuildNumber : androidVersionCode,
runtimeVersion,
androidPackageName,
// SDK
bundleId: IS_IOS ? iosBundleIdentifier : androidPackageName,
apiURL,
// SDKs
sentryDsn,
mixpanelToken,
flagsmithKey,
Expand Down
4 changes: 4 additions & 0 deletions src/core/constants/storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export const storageKeys = {
appStorage: {
id: 'app-storage',
locale: 'app.locale',
},
queryStorage: {
id: 'query-storage',
},
};
16 changes: 16 additions & 0 deletions src/core/i18n/utils/getCurrentLocale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { i18n } from 'i18next';

import { config } from '$core/constants';

export const getCurrentLocale = (i18n: i18n) => {
const languageCode = i18n.language;
const [primaryCode] = languageCode.split('-');

if (i18n.hasResourceBundle(languageCode, 'common')) {
return languageCode;
} else if (primaryCode && i18n.hasResourceBundle(primaryCode, 'common')) {
return primaryCode;
}

return config.defaultLocale;
};
4 changes: 3 additions & 1 deletion src/core/storage/appStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { MMKV } from 'react-native-mmkv';

import { storageKeys } from '$core/constants';

export const AppStorage = new MMKV({
id: 'app-storage',
id: storageKeys.appStorage.id,
});
1 change: 1 addition & 0 deletions src/core/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { AppStorage } from './appStorage';
export { QueryClientStorage } from './queryClientStorage';
Loading

0 comments on commit 95bb193

Please sign in to comment.