shipos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -0
- package/dist/cache.d.ts +29 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +214 -0
- package/dist/modules/configs.d.ts +56 -0
- package/dist/modules/configs.d.ts.map +1 -0
- package/dist/modules/customers.d.ts +76 -0
- package/dist/modules/customers.d.ts.map +1 -0
- package/dist/modules/feature-flags.d.ts +66 -0
- package/dist/modules/feature-flags.d.ts.map +1 -0
- package/dist/react/components.d.ts +85 -0
- package/dist/react/components.d.ts.map +1 -0
- package/dist/react/hooks.d.ts +115 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +388 -0
- package/dist/react/provider.d.ts +61 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Flags Module
|
|
3
|
+
*
|
|
4
|
+
* Provides namespaced access to feature flag operations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* const flags = await shipos.featureFlags.getAll();
|
|
9
|
+
* const isEnabled = await shipos.featureFlags.get('new-feature', false);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import type { Cache } from "../cache";
|
|
13
|
+
import type { FlagsResponse } from "../types";
|
|
14
|
+
/**
|
|
15
|
+
* Internal context shared by the parent ShipOS client
|
|
16
|
+
*/
|
|
17
|
+
export interface FeatureFlagsContext {
|
|
18
|
+
request: <T>(endpoint: string, options?: {
|
|
19
|
+
method?: string;
|
|
20
|
+
body?: unknown;
|
|
21
|
+
}) => Promise<T>;
|
|
22
|
+
cache: Cache;
|
|
23
|
+
getEnvironment: () => string;
|
|
24
|
+
log: (...args: unknown[]) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare class FeatureFlagsModule {
|
|
27
|
+
private ctx;
|
|
28
|
+
constructor(ctx: FeatureFlagsContext);
|
|
29
|
+
/**
|
|
30
|
+
* Get all feature flags
|
|
31
|
+
*
|
|
32
|
+
* @returns Record of flag keys to boolean values
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const flags = await shipos.featureFlags.getAll();
|
|
37
|
+
* // => { "new-dashboard": true, "beta-features": false }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
getAll(): Promise<FlagsResponse>;
|
|
41
|
+
/**
|
|
42
|
+
* Get a single feature flag value
|
|
43
|
+
*
|
|
44
|
+
* @param key - The flag key
|
|
45
|
+
* @param defaultValue - Default value if flag is not found (defaults to false)
|
|
46
|
+
* @returns The flag value or default
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const isEnabled = await shipos.featureFlags.get('new-dashboard', false);
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
get(key: string, defaultValue?: boolean): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Refresh flags (bypass cache)
|
|
56
|
+
*
|
|
57
|
+
* @returns Fresh flags from the API
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const freshFlags = await shipos.featureFlags.refresh();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
refresh(): Promise<FlagsResponse>;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=feature-flags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flags.d.ts","sourceRoot":"","sources":["../../src/modules/feature-flags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5F,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACnC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,GAAG,CAAsB;gBAErB,GAAG,EAAE,mBAAmB;IAIpC;;;;;;;;;;OAUG;IACG,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC;IActC;;;;;;;;;;;OAWG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAKvE;;;;;;;;;OASG;IACG,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC;CAKxC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Feature Component Props
|
|
4
|
+
*/
|
|
5
|
+
interface FeatureProps {
|
|
6
|
+
/** The feature flag key */
|
|
7
|
+
flag: string;
|
|
8
|
+
/** Content to render when the flag is enabled */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/** Content to render when the flag is disabled (optional) */
|
|
11
|
+
fallback?: ReactNode;
|
|
12
|
+
/** Content to render while loading (optional) */
|
|
13
|
+
loading?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Feature Component
|
|
17
|
+
*
|
|
18
|
+
* Conditionally renders content based on a feature flag.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // Basic usage
|
|
23
|
+
* <Feature flag="new-dashboard">
|
|
24
|
+
* <NewDashboard />
|
|
25
|
+
* </Feature>
|
|
26
|
+
*
|
|
27
|
+
* // With fallback
|
|
28
|
+
* <Feature flag="new-dashboard" fallback={<OldDashboard />}>
|
|
29
|
+
* <NewDashboard />
|
|
30
|
+
* </Feature>
|
|
31
|
+
*
|
|
32
|
+
* // With loading state
|
|
33
|
+
* <Feature flag="new-dashboard" loading={<Skeleton />}>
|
|
34
|
+
* <NewDashboard />
|
|
35
|
+
* </Feature>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function Feature({ flag, children, fallback, loading, }: FeatureProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
/**
|
|
40
|
+
* FeatureEnabled Component Props
|
|
41
|
+
*/
|
|
42
|
+
interface FeatureEnabledProps {
|
|
43
|
+
/** The feature flag key */
|
|
44
|
+
flag: string;
|
|
45
|
+
/** Content to render when the flag is enabled */
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* FeatureEnabled Component
|
|
50
|
+
*
|
|
51
|
+
* Only renders children when the flag is enabled.
|
|
52
|
+
* Simpler alternative to Feature when you don't need fallback.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <FeatureEnabled flag="beta-features">
|
|
57
|
+
* <BetaBanner />
|
|
58
|
+
* </FeatureEnabled>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function FeatureEnabled({ flag, children }: FeatureEnabledProps): import("react/jsx-runtime").JSX.Element | null;
|
|
62
|
+
/**
|
|
63
|
+
* FeatureDisabled Component Props
|
|
64
|
+
*/
|
|
65
|
+
interface FeatureDisabledProps {
|
|
66
|
+
/** The feature flag key */
|
|
67
|
+
flag: string;
|
|
68
|
+
/** Content to render when the flag is disabled */
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* FeatureDisabled Component
|
|
73
|
+
*
|
|
74
|
+
* Only renders children when the flag is disabled.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* <FeatureDisabled flag="maintenance-mode">
|
|
79
|
+
* <MainContent />
|
|
80
|
+
* </FeatureDisabled>
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function FeatureDisabled({ flag, children }: FeatureDisabledProps): import("react/jsx-runtime").JSX.Element | null;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/react/components.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC;;GAEG;AACH,UAAU,YAAY;IACpB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,QAAQ,EAAE,SAAS,CAAC;IACpB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CAAC,EACtB,IAAI,EACJ,QAAQ,EACR,QAAe,EACf,OAAc,GACf,EAAE,YAAY,2CAQd;AAED;;GAEG;AACH,UAAU,mBAAmB;IAC3B,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,mBAAmB,kDAQrE;AAED;;GAEG;AACH,UAAU,oBAAoB;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,oBAAoB,kDAQvE"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { CustomerInfo, SignInOptions } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Hook to get all feature flags
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```tsx
|
|
7
|
+
* function MyComponent() {
|
|
8
|
+
* const { flags, isLoading, error } = useFlags();
|
|
9
|
+
*
|
|
10
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
11
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
12
|
+
*
|
|
13
|
+
* return (
|
|
14
|
+
* <div>
|
|
15
|
+
* {flags['new-feature'] && <NewFeature />}
|
|
16
|
+
* </div>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function useFlags(): {
|
|
22
|
+
flags: import("..").FlagsResponse;
|
|
23
|
+
isLoading: boolean;
|
|
24
|
+
error: Error | null;
|
|
25
|
+
refreshFlags: () => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Hook to get a single feature flag
|
|
29
|
+
*
|
|
30
|
+
* @param key - The flag key
|
|
31
|
+
* @param defaultValue - Default value if flag is not found
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* function MyComponent() {
|
|
36
|
+
* const { enabled, isLoading } = useFlag('new-feature', false);
|
|
37
|
+
*
|
|
38
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
39
|
+
*
|
|
40
|
+
* return enabled ? <NewFeature /> : <OldFeature />;
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function useFlag(key: string, defaultValue?: boolean): {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
error: Error | null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Hook to get a configuration by slug
|
|
51
|
+
*
|
|
52
|
+
* @param slug - The config slug
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* interface PricingConfig {
|
|
57
|
+
* plans: Array<{ name: string; price: number }>;
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* function PricingPage() {
|
|
61
|
+
* const { config, isLoading, error, refresh } = useConfig<PricingConfig>('pricing');
|
|
62
|
+
*
|
|
63
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
64
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
65
|
+
*
|
|
66
|
+
* return (
|
|
67
|
+
* <div>
|
|
68
|
+
* {config?.plans.map(plan => (
|
|
69
|
+
* <div key={plan.name}>{plan.name}: ${plan.price}</div>
|
|
70
|
+
* ))}
|
|
71
|
+
* </div>
|
|
72
|
+
* );
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function useConfig<T = Record<string, unknown>>(slug: string): {
|
|
77
|
+
config: T | null;
|
|
78
|
+
isLoading: boolean;
|
|
79
|
+
error: Error | null;
|
|
80
|
+
refresh: () => Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Hook to manage customer identification
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* function UserProfile() {
|
|
88
|
+
* const { customer, signIn, signOut } = useCustomer();
|
|
89
|
+
*
|
|
90
|
+
* useEffect(() => {
|
|
91
|
+
* // Sign in when user is authenticated
|
|
92
|
+
* signIn({ externalId: 'user-123', metadata: { plan: 'pro' } });
|
|
93
|
+
* }, []);
|
|
94
|
+
*
|
|
95
|
+
* return (
|
|
96
|
+
* <div>
|
|
97
|
+
* {customer ? (
|
|
98
|
+
* <>
|
|
99
|
+
* <p>Signed in as: {customer.externalId}</p>
|
|
100
|
+
* <button onClick={signOut}>Sign Out</button>
|
|
101
|
+
* </>
|
|
102
|
+
* ) : (
|
|
103
|
+
* <p>Not signed in</p>
|
|
104
|
+
* )}
|
|
105
|
+
* </div>
|
|
106
|
+
* );
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function useCustomer(): {
|
|
111
|
+
customer: CustomerInfo | null;
|
|
112
|
+
signIn: (options: SignInOptions) => Promise<CustomerInfo>;
|
|
113
|
+
signOut: () => void;
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/react/hooks.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAkB,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE5E;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ;;;;;EAGvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,OAAe;;;;EAKjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM;;;;;EA0ClE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,WAAW,IAUpB;IACH,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC1D,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShipOS SDK React Integration
|
|
3
|
+
*
|
|
4
|
+
* React hooks and components for ShipOS feature flags, configs, and customer identification.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { ShipOSProvider, useFlag, useCustomer, Feature } from '@shipos/sdk/react';
|
|
9
|
+
*
|
|
10
|
+
* function App() {
|
|
11
|
+
* return (
|
|
12
|
+
* <ShipOSProvider config={{ apiKey: 'your-api-key' }}>
|
|
13
|
+
* <Feature flag="new-feature">
|
|
14
|
+
* <NewFeature />
|
|
15
|
+
* </Feature>
|
|
16
|
+
* </ShipOSProvider>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { ShipOSProvider, useShipOS } from "./provider";
|
|
22
|
+
export { useFlags, useFlag, useConfig, useCustomer } from "./hooks";
|
|
23
|
+
export { Feature, FeatureEnabled, FeatureDisabled } from "./components";
|
|
24
|
+
export type { ShipOSConfig, FlagsResponse, ConfigResponse, CustomerInfo, SignInOptions, } from "../types";
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGpE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGxE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,GACd,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
class Cache {
|
|
3
|
+
store = new Map;
|
|
4
|
+
defaultTTL;
|
|
5
|
+
constructor(defaultTTL = 60000) {
|
|
6
|
+
this.defaultTTL = defaultTTL;
|
|
7
|
+
}
|
|
8
|
+
get(key) {
|
|
9
|
+
const entry = this.store.get(key);
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (Date.now() - entry.timestamp > this.defaultTTL) {
|
|
14
|
+
this.store.delete(key);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return entry.data;
|
|
18
|
+
}
|
|
19
|
+
set(key, data) {
|
|
20
|
+
this.store.set(key, {
|
|
21
|
+
data,
|
|
22
|
+
timestamp: Date.now()
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
delete(key) {
|
|
26
|
+
this.store.delete(key);
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.store.clear();
|
|
30
|
+
}
|
|
31
|
+
setTTL(ttl) {
|
|
32
|
+
this.defaultTTL = ttl;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/modules/feature-flags.ts
|
|
37
|
+
class FeatureFlagsModule {
|
|
38
|
+
ctx;
|
|
39
|
+
constructor(ctx) {
|
|
40
|
+
this.ctx = ctx;
|
|
41
|
+
}
|
|
42
|
+
async getAll() {
|
|
43
|
+
const cacheKey = `flags:${this.ctx.getEnvironment()}`;
|
|
44
|
+
const cached = this.ctx.cache.get(cacheKey);
|
|
45
|
+
if (cached) {
|
|
46
|
+
this.ctx.log("Returning cached flags");
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
const flags = await this.ctx.request("/v1/flags");
|
|
50
|
+
this.ctx.cache.set(cacheKey, flags);
|
|
51
|
+
return flags;
|
|
52
|
+
}
|
|
53
|
+
async get(key, defaultValue = false) {
|
|
54
|
+
const flags = await this.getAll();
|
|
55
|
+
return flags[key] ?? defaultValue;
|
|
56
|
+
}
|
|
57
|
+
async refresh() {
|
|
58
|
+
const cacheKey = `flags:${this.ctx.getEnvironment()}`;
|
|
59
|
+
this.ctx.cache.delete(cacheKey);
|
|
60
|
+
return this.getAll();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/modules/configs.ts
|
|
65
|
+
class ConfigsModule {
|
|
66
|
+
ctx;
|
|
67
|
+
constructor(ctx) {
|
|
68
|
+
this.ctx = ctx;
|
|
69
|
+
}
|
|
70
|
+
async get(slug) {
|
|
71
|
+
const cacheKey = `config:${this.ctx.getEnvironment()}:${slug}`;
|
|
72
|
+
const cached = this.ctx.cache.get(cacheKey);
|
|
73
|
+
if (cached) {
|
|
74
|
+
this.ctx.log(`Returning cached config: ${slug}`);
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
const config = await this.ctx.request(`/v1/config/${slug}`);
|
|
78
|
+
this.ctx.cache.set(cacheKey, config);
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
async refresh(slug) {
|
|
82
|
+
const cacheKey = `config:${this.ctx.getEnvironment()}:${slug}`;
|
|
83
|
+
this.ctx.cache.delete(cacheKey);
|
|
84
|
+
return this.get(slug);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/modules/customers.ts
|
|
89
|
+
class CustomersModule {
|
|
90
|
+
ctx;
|
|
91
|
+
customer = null;
|
|
92
|
+
constructor(ctx) {
|
|
93
|
+
this.ctx = ctx;
|
|
94
|
+
}
|
|
95
|
+
async signIn(options) {
|
|
96
|
+
if (!options.externalId) {
|
|
97
|
+
throw new Error("ShipOS: externalId is required for signIn");
|
|
98
|
+
}
|
|
99
|
+
this.ctx.log(`Signing in customer: ${options.externalId}`);
|
|
100
|
+
this.customer = {
|
|
101
|
+
externalId: options.externalId,
|
|
102
|
+
metadata: options.metadata
|
|
103
|
+
};
|
|
104
|
+
this.ctx.setCustomerId(options.externalId);
|
|
105
|
+
await this.ctx.request("/v1/customers/identify", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: {
|
|
108
|
+
externalId: options.externalId,
|
|
109
|
+
metadata: options.metadata ?? {}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
this.ctx.log(`Customer signed in: ${options.externalId}`);
|
|
113
|
+
return this.customer;
|
|
114
|
+
}
|
|
115
|
+
signOut() {
|
|
116
|
+
this.ctx.log("Signing out customer");
|
|
117
|
+
this.customer = null;
|
|
118
|
+
this.ctx.setCustomerId(null);
|
|
119
|
+
}
|
|
120
|
+
get() {
|
|
121
|
+
return this.customer;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/client.ts
|
|
126
|
+
var DEFAULT_BASE_URL = "https://api.shipos.app";
|
|
127
|
+
var DEFAULT_CACHE_TTL = 60000;
|
|
128
|
+
var SDK_VERSION = "0.1.0";
|
|
129
|
+
|
|
130
|
+
class ShipOS {
|
|
131
|
+
apiKey;
|
|
132
|
+
baseUrl;
|
|
133
|
+
environment;
|
|
134
|
+
cache;
|
|
135
|
+
debug;
|
|
136
|
+
customerId = null;
|
|
137
|
+
featureFlags;
|
|
138
|
+
configs;
|
|
139
|
+
customers;
|
|
140
|
+
constructor(config) {
|
|
141
|
+
if (!config.apiKey) {
|
|
142
|
+
throw new Error("ShipOS: apiKey is required");
|
|
143
|
+
}
|
|
144
|
+
this.apiKey = config.apiKey;
|
|
145
|
+
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
146
|
+
this.environment = config.environment || "production";
|
|
147
|
+
this.cache = new Cache(config.cacheTTL || DEFAULT_CACHE_TTL);
|
|
148
|
+
this.debug = config.debug || false;
|
|
149
|
+
const ctx = {
|
|
150
|
+
request: this.request.bind(this),
|
|
151
|
+
cache: this.cache,
|
|
152
|
+
getEnvironment: this.getEnvironment.bind(this),
|
|
153
|
+
log: this.log.bind(this),
|
|
154
|
+
setCustomerId: (id) => {
|
|
155
|
+
this.customerId = id;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
this.featureFlags = new FeatureFlagsModule(ctx);
|
|
159
|
+
this.configs = new ConfigsModule(ctx);
|
|
160
|
+
this.customers = new CustomersModule(ctx);
|
|
161
|
+
}
|
|
162
|
+
log(...args) {
|
|
163
|
+
if (this.debug) {
|
|
164
|
+
console.log("[ShipOS]", ...args);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async request(endpoint, options = {}) {
|
|
168
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
169
|
+
const method = options.method || "GET";
|
|
170
|
+
this.log(`${method} ${url}`);
|
|
171
|
+
const headers = {
|
|
172
|
+
"x-shipos-key": this.apiKey,
|
|
173
|
+
"x-shipos-env": this.environment,
|
|
174
|
+
"x-shipos-sdk-version": SDK_VERSION,
|
|
175
|
+
"Content-Type": "application/json"
|
|
176
|
+
};
|
|
177
|
+
if (this.customerId) {
|
|
178
|
+
headers["x-shipos-customer-id"] = this.customerId;
|
|
179
|
+
}
|
|
180
|
+
const fetchOptions = { method, headers };
|
|
181
|
+
if (options.body && method !== "GET") {
|
|
182
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
183
|
+
}
|
|
184
|
+
const response = await fetch(url, fetchOptions);
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
const error = await response.json().catch(() => ({
|
|
187
|
+
error: `HTTP ${response.status}`,
|
|
188
|
+
code: "HTTP_ERROR"
|
|
189
|
+
}));
|
|
190
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
191
|
+
}
|
|
192
|
+
return response.json();
|
|
193
|
+
}
|
|
194
|
+
clearCache() {
|
|
195
|
+
this.cache.clear();
|
|
196
|
+
this.log("Cache cleared");
|
|
197
|
+
}
|
|
198
|
+
setEnvironment(environment) {
|
|
199
|
+
this.environment = environment;
|
|
200
|
+
this.cache.clear();
|
|
201
|
+
this.log(`Environment changed to: ${environment}`);
|
|
202
|
+
}
|
|
203
|
+
getEnvironment() {
|
|
204
|
+
return this.environment;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function createShipOS(config) {
|
|
208
|
+
return new ShipOS(config);
|
|
209
|
+
}
|
|
210
|
+
// src/react/provider.tsx
|
|
211
|
+
import {
|
|
212
|
+
createContext,
|
|
213
|
+
useContext,
|
|
214
|
+
useState,
|
|
215
|
+
useEffect,
|
|
216
|
+
useCallback
|
|
217
|
+
} from "react";
|
|
218
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
219
|
+
"use client";
|
|
220
|
+
var ShipOSContext = createContext(null);
|
|
221
|
+
function ShipOSProvider({
|
|
222
|
+
config,
|
|
223
|
+
children,
|
|
224
|
+
initialFlags = {}
|
|
225
|
+
}) {
|
|
226
|
+
const [client] = useState(() => new ShipOS(config));
|
|
227
|
+
const [flags, setFlags] = useState(initialFlags);
|
|
228
|
+
const [isLoading, setIsLoading] = useState(Object.keys(initialFlags).length === 0);
|
|
229
|
+
const [error, setError] = useState(null);
|
|
230
|
+
const [customer, setCustomer] = useState(null);
|
|
231
|
+
const fetchFlags = useCallback(async () => {
|
|
232
|
+
try {
|
|
233
|
+
setIsLoading(true);
|
|
234
|
+
setError(null);
|
|
235
|
+
const newFlags = await client.featureFlags.getAll();
|
|
236
|
+
setFlags(newFlags);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch flags"));
|
|
239
|
+
} finally {
|
|
240
|
+
setIsLoading(false);
|
|
241
|
+
}
|
|
242
|
+
}, [client]);
|
|
243
|
+
const refreshFlags = useCallback(async () => {
|
|
244
|
+
try {
|
|
245
|
+
setError(null);
|
|
246
|
+
const newFlags = await client.featureFlags.refresh();
|
|
247
|
+
setFlags(newFlags);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
setError(err instanceof Error ? err : new Error("Failed to refresh flags"));
|
|
250
|
+
}
|
|
251
|
+
}, [client]);
|
|
252
|
+
const signIn = useCallback(async (options) => {
|
|
253
|
+
const customerInfo = await client.customers.signIn(options);
|
|
254
|
+
setCustomer(customerInfo);
|
|
255
|
+
return customerInfo;
|
|
256
|
+
}, [client]);
|
|
257
|
+
const signOut = useCallback(() => {
|
|
258
|
+
client.customers.signOut();
|
|
259
|
+
setCustomer(null);
|
|
260
|
+
}, [client]);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (Object.keys(initialFlags).length === 0) {
|
|
263
|
+
fetchFlags();
|
|
264
|
+
}
|
|
265
|
+
}, [fetchFlags, initialFlags]);
|
|
266
|
+
const value = {
|
|
267
|
+
client,
|
|
268
|
+
flags,
|
|
269
|
+
isLoading,
|
|
270
|
+
error,
|
|
271
|
+
refreshFlags,
|
|
272
|
+
customer,
|
|
273
|
+
signIn,
|
|
274
|
+
signOut
|
|
275
|
+
};
|
|
276
|
+
return /* @__PURE__ */ jsxDEV(ShipOSContext.Provider, {
|
|
277
|
+
value,
|
|
278
|
+
children
|
|
279
|
+
}, undefined, false, undefined, this);
|
|
280
|
+
}
|
|
281
|
+
function useShipOS() {
|
|
282
|
+
const context = useContext(ShipOSContext);
|
|
283
|
+
if (!context) {
|
|
284
|
+
throw new Error("useShipOS must be used within a ShipOSProvider");
|
|
285
|
+
}
|
|
286
|
+
return context;
|
|
287
|
+
}
|
|
288
|
+
// src/react/hooks.ts
|
|
289
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
290
|
+
"use client";
|
|
291
|
+
function useFlags() {
|
|
292
|
+
const { flags, isLoading, error, refreshFlags } = useShipOS();
|
|
293
|
+
return { flags, isLoading, error, refreshFlags };
|
|
294
|
+
}
|
|
295
|
+
function useFlag(key, defaultValue = false) {
|
|
296
|
+
const { flags, isLoading, error } = useShipOS();
|
|
297
|
+
const enabled = flags[key] ?? defaultValue;
|
|
298
|
+
return { enabled, isLoading, error };
|
|
299
|
+
}
|
|
300
|
+
function useConfig(slug) {
|
|
301
|
+
const { client } = useShipOS();
|
|
302
|
+
const [config, setConfig] = useState2(null);
|
|
303
|
+
const [isLoading, setIsLoading] = useState2(true);
|
|
304
|
+
const [error, setError] = useState2(null);
|
|
305
|
+
const fetchConfig = useCallback2(async () => {
|
|
306
|
+
try {
|
|
307
|
+
setIsLoading(true);
|
|
308
|
+
setError(null);
|
|
309
|
+
const data = await client.configs.get(slug);
|
|
310
|
+
setConfig(data);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
setError(err instanceof Error ? err : new Error(`Failed to fetch config: ${slug}`));
|
|
313
|
+
} finally {
|
|
314
|
+
setIsLoading(false);
|
|
315
|
+
}
|
|
316
|
+
}, [client, slug]);
|
|
317
|
+
const refresh = useCallback2(async () => {
|
|
318
|
+
try {
|
|
319
|
+
setError(null);
|
|
320
|
+
const data = await client.configs.refresh(slug);
|
|
321
|
+
setConfig(data);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
setError(err instanceof Error ? err : new Error(`Failed to refresh config: ${slug}`));
|
|
324
|
+
}
|
|
325
|
+
}, [client, slug]);
|
|
326
|
+
useEffect2(() => {
|
|
327
|
+
fetchConfig();
|
|
328
|
+
}, [fetchConfig]);
|
|
329
|
+
return { config, isLoading, error, refresh };
|
|
330
|
+
}
|
|
331
|
+
function useCustomer() {
|
|
332
|
+
const { customer, signIn, signOut } = useShipOS();
|
|
333
|
+
return {
|
|
334
|
+
customer,
|
|
335
|
+
signIn,
|
|
336
|
+
signOut
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// src/react/components.tsx
|
|
340
|
+
import { jsxDEV as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
|
|
341
|
+
"use client";
|
|
342
|
+
function Feature({
|
|
343
|
+
flag,
|
|
344
|
+
children,
|
|
345
|
+
fallback = null,
|
|
346
|
+
loading = null
|
|
347
|
+
}) {
|
|
348
|
+
const { enabled, isLoading } = useFlag(flag);
|
|
349
|
+
if (isLoading && loading) {
|
|
350
|
+
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
351
|
+
children: loading
|
|
352
|
+
}, undefined, false, undefined, this);
|
|
353
|
+
}
|
|
354
|
+
return enabled ? /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
355
|
+
children
|
|
356
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
357
|
+
children: fallback
|
|
358
|
+
}, undefined, false, undefined, this);
|
|
359
|
+
}
|
|
360
|
+
function FeatureEnabled({ flag, children }) {
|
|
361
|
+
const { enabled, isLoading } = useFlag(flag);
|
|
362
|
+
if (isLoading || !enabled) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
366
|
+
children
|
|
367
|
+
}, undefined, false, undefined, this);
|
|
368
|
+
}
|
|
369
|
+
function FeatureDisabled({ flag, children }) {
|
|
370
|
+
const { enabled, isLoading } = useFlag(flag);
|
|
371
|
+
if (isLoading || enabled) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
375
|
+
children
|
|
376
|
+
}, undefined, false, undefined, this);
|
|
377
|
+
}
|
|
378
|
+
export {
|
|
379
|
+
useShipOS,
|
|
380
|
+
useFlags,
|
|
381
|
+
useFlag,
|
|
382
|
+
useCustomer,
|
|
383
|
+
useConfig,
|
|
384
|
+
ShipOSProvider,
|
|
385
|
+
FeatureEnabled,
|
|
386
|
+
FeatureDisabled,
|
|
387
|
+
Feature
|
|
388
|
+
};
|