@farcaster/frame-sdk 0.0.63 → 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/src/quickAuth.ts DELETED
@@ -1,148 +0,0 @@
1
- import { SignIn } from '@farcaster/frame-core'
2
- import type { JWTPayload } from '@farcaster/quick-auth'
3
- import { decodeJwt } from '@farcaster/quick-auth/decodeJwt'
4
- import { createLightClient } from '@farcaster/quick-auth/light'
5
- import * as Siwe from 'ox/Siwe'
6
-
7
- import { frameHost } from './frameHost.js'
8
-
9
- export declare namespace getToken {
10
- type Options = {
11
- /**
12
- * Use a custom Quick Auth server, otherwise defaults to the public
13
- * instance provided by Farcaster.
14
- *
15
- * @default https://auth.farcaster.xyz
16
- */
17
- quickAuthServerOrigin?: string
18
-
19
- /**
20
- * Acquire a new token even if one is already in memory and not expired.
21
- *
22
- * @default false
23
- */
24
- force?: string
25
- }
26
-
27
- type ReturnValue = { token: string }
28
- type ReturnType = Promise<ReturnValue>
29
-
30
- type ErrorType = SignIn.RejectedByUser | Error
31
- }
32
-
33
- export type QuickAuth = {
34
- /**
35
- * Returns the session token if one is present and not expired.
36
- */
37
- readonly token: string | undefined
38
-
39
- /**
40
- * Get a Quick Auth JWT.
41
- *
42
- * If a token is already in memory and not expired it will be returned unless
43
- * `force` is used. Otherwise a new token will be acquired.
44
- */
45
- getToken: (options?: getToken.Options) => getToken.ReturnType
46
-
47
- /**
48
- * Make an authenticated fetch request. The `Authorization` header will
49
- * contain `Bearer ${token}` where token is a Quick Auth session token.
50
- */
51
- fetch: typeof fetch
52
- }
53
-
54
- export const quickAuth: QuickAuth = (() => {
55
- let current:
56
- | {
57
- token: string
58
- payload: JWTPayload
59
- }
60
- | undefined
61
- let pendingPromise: Promise<getToken.ReturnValue> | undefined = undefined
62
-
63
- async function getTokenInner(options: getToken.Options): getToken.ReturnType {
64
- const quickAuthClient = createLightClient({
65
- origin: options.quickAuthServerOrigin,
66
- })
67
-
68
- const { nonce } = await quickAuthClient.generateNonce()
69
- const response = await frameHost.signIn({
70
- nonce,
71
- acceptAuthAddress: true,
72
- })
73
-
74
- if (response.result) {
75
- const parsedSiwe = Siwe.parseMessage(response.result.message)
76
-
77
- // The Farcaster Client rendering the Mini App will set the domain
78
- // based on the URL it's rendering. It should always be set.
79
- if (!parsedSiwe.domain) {
80
- throw new Error('Missing domain on SIWE message')
81
- }
82
-
83
- const verifyResult = await quickAuthClient.verifySiwf({
84
- domain: parsedSiwe.domain,
85
- message: response.result.message,
86
- signature: response.result.signature,
87
- })
88
-
89
- current = {
90
- token: verifyResult.token,
91
- payload: decodeJwt(verifyResult.token),
92
- }
93
-
94
- return verifyResult
95
- }
96
-
97
- if (response.error.type === 'rejected_by_user') {
98
- throw new SignIn.RejectedByUser()
99
- }
100
-
101
- throw new Error('Unreachable')
102
- }
103
-
104
- return {
105
- get token() {
106
- if (
107
- current &&
108
- new Date(current.payload.exp * 1000) > new Date(Date.now() + 15000)
109
- ) {
110
- return current.token
111
- }
112
-
113
- return undefined
114
- },
115
- async getToken(options = {}) {
116
- const force = options.force ?? false
117
-
118
- if (
119
- current &&
120
- !force &&
121
- new Date(current.payload.exp * 1000) > new Date(Date.now() + 15000)
122
- ) {
123
- return { token: current.token }
124
- }
125
-
126
- if (!pendingPromise) {
127
- pendingPromise = getTokenInner(options)
128
- }
129
-
130
- pendingPromise.finally(() => {
131
- pendingPromise = undefined
132
- })
133
-
134
- return pendingPromise
135
- },
136
- async fetch(url, options) {
137
- const { token } = await this.getToken()
138
-
139
- const headers = new Headers(options?.headers)
140
- headers.set('Authorization', `Bearer ${token}`)
141
-
142
- return fetch(url, {
143
- ...options,
144
- headers,
145
- })
146
- },
147
- }
148
- })()
package/src/sdk.ts DELETED
@@ -1,191 +0,0 @@
1
- import {
2
- AddMiniApp,
3
- type FrameClientEvent,
4
- SignIn,
5
- } from '@farcaster/frame-core'
6
- import { createBack } from './back.ts'
7
- import { ethereumProvider, getEthereumProvider } from './ethereumProvider.ts'
8
- import { frameHost } from './frameHost.ts'
9
- import { quickAuth } from './quickAuth.ts'
10
- import { emitter } from './sdkEmitter.ts'
11
- import { getSolanaProvider } from './solanaProvider.ts'
12
- import type { FrameSDK } from './types.ts'
13
-
14
- let cachedIsInMiniAppResult: boolean | null = null
15
-
16
- /**
17
- * Determines if the current environment is a MiniApp context.
18
- *
19
- * @param timeoutMs - Optional timeout in milliseconds (default: 50)
20
- * @returns Promise resolving to boolean indicating if in MiniApp context
21
- */
22
- async function isInMiniApp(timeoutMs = 50): Promise<boolean> {
23
- // Return cached result if we've already determined we are in a MiniApp
24
- if (cachedIsInMiniAppResult === true) {
25
- return true
26
- }
27
-
28
- // Check for SSR environment - definitely not a MiniApp
29
- if (typeof window === 'undefined') {
30
- return false
31
- }
32
-
33
- // Short-circuit: definitely NOT a MiniApp
34
- if (!window.ReactNativeWebView && window === window.parent) {
35
- return false
36
- }
37
-
38
- // At this point, we MIGHT be in a MiniApp (iframe or RN WebView)
39
- // but need to verify by checking for context communication.
40
- const isInMiniApp = await Promise.race([
41
- frameHost.context.then((context) => !!context), // Check if context resolves to truthy
42
- new Promise<boolean>((resolve) => {
43
- setTimeout(() => resolve(false), timeoutMs) // Timeout resolves to false
44
- }),
45
- ]).catch(() => {
46
- return false
47
- })
48
-
49
- // Cache the result ONLY if true (we are confirmed to be in a MiniApp)
50
- if (isInMiniApp) {
51
- cachedIsInMiniAppResult = true
52
- }
53
-
54
- return isInMiniApp
55
- }
56
-
57
- const addMiniApp = async () => {
58
- const response = await frameHost.addFrame()
59
- if (response.result) {
60
- return response.result
61
- }
62
-
63
- if (response.error.type === 'invalid_domain_manifest') {
64
- throw new AddMiniApp.InvalidDomainManifest()
65
- }
66
-
67
- if (response.error.type === 'rejected_by_user') {
68
- throw new AddMiniApp.RejectedByUser()
69
- }
70
-
71
- throw new Error('Unreachable')
72
- }
73
-
74
- export const sdk: FrameSDK = {
75
- ...emitter,
76
- getCapabilities: frameHost.getCapabilities,
77
- getChains: frameHost.getChains,
78
- isInMiniApp,
79
- context: frameHost.context,
80
- back: createBack({ frameHost, emitter }),
81
- quickAuth,
82
- actions: {
83
- setPrimaryButton: frameHost.setPrimaryButton.bind(frameHost),
84
- ready: async (options = {}) => {
85
- return await frameHost.ready(options)
86
- },
87
- close: frameHost.close.bind(frameHost),
88
- viewCast: frameHost.viewCast.bind(frameHost),
89
- viewProfile: frameHost.viewProfile.bind(frameHost),
90
- signIn: async (options) => {
91
- const response = await frameHost.signIn(options)
92
- if (response.result) {
93
- return response.result
94
- }
95
-
96
- if (response.error.type === 'rejected_by_user') {
97
- throw new SignIn.RejectedByUser()
98
- }
99
-
100
- throw new Error('Unreachable')
101
- },
102
- openUrl: (urlArg: string | { url: string }) => {
103
- const url = typeof urlArg === 'string' ? urlArg : urlArg.url
104
- return frameHost.openUrl(url.trim())
105
- },
106
- addFrame: addMiniApp,
107
- addMiniApp,
108
- composeCast(options = {}) {
109
- return frameHost.composeCast(options) as never
110
- },
111
- viewToken: frameHost.viewToken.bind(frameHost),
112
- sendToken: frameHost.sendToken.bind(frameHost),
113
- swapToken: frameHost.swapToken.bind(frameHost),
114
- },
115
- experimental: {
116
- getSolanaProvider,
117
- quickAuth(options) {
118
- return quickAuth.getToken(options)
119
- },
120
- },
121
- wallet: {
122
- ethProvider: ethereumProvider,
123
- getEthereumProvider,
124
- getSolanaProvider,
125
- },
126
- haptics: {
127
- impactOccurred: frameHost.impactOccurred.bind(frameHost),
128
- notificationOccurred: frameHost.notificationOccurred.bind(frameHost),
129
- selectionChanged: frameHost.selectionChanged.bind(frameHost),
130
- },
131
- }
132
-
133
- // Required to pass SSR
134
- if (typeof document !== 'undefined') {
135
- // react native webview events
136
- document.addEventListener('FarcasterFrameEvent', (event) => {
137
- if (event instanceof MessageEvent) {
138
- const frameEvent = event.data as FrameClientEvent
139
- if (frameEvent.event === 'primary_button_clicked') {
140
- emitter.emit('primaryButtonClicked')
141
- } else if (frameEvent.event === 'frame_added') {
142
- emitter.emit('frameAdded', {
143
- notificationDetails: frameEvent.notificationDetails,
144
- })
145
- } else if (frameEvent.event === 'frame_add_rejected') {
146
- emitter.emit('frameAddRejected', { reason: frameEvent.reason })
147
- } else if (frameEvent.event === 'frame_removed') {
148
- emitter.emit('frameRemoved')
149
- } else if (frameEvent.event === 'notifications_enabled') {
150
- emitter.emit('notificationsEnabled', {
151
- notificationDetails: frameEvent.notificationDetails,
152
- })
153
- } else if (frameEvent.event === 'notifications_disabled') {
154
- emitter.emit('notificationsDisabled')
155
- } else if (frameEvent.event === 'back_navigation_triggered') {
156
- emitter.emit('backNavigationTriggered')
157
- }
158
- }
159
- })
160
- }
161
-
162
- // Required to pass SSR
163
- if (typeof window !== 'undefined') {
164
- // web events
165
- window.addEventListener('message', (event) => {
166
- if (event instanceof MessageEvent) {
167
- if (event.data.type === 'frameEvent') {
168
- const frameEvent = event.data.event as FrameClientEvent
169
- if (frameEvent.event === 'primary_button_clicked') {
170
- emitter.emit('primaryButtonClicked')
171
- } else if (frameEvent.event === 'frame_added') {
172
- emitter.emit('frameAdded', {
173
- notificationDetails: frameEvent.notificationDetails,
174
- })
175
- } else if (frameEvent.event === 'frame_add_rejected') {
176
- emitter.emit('frameAddRejected', { reason: frameEvent.reason })
177
- } else if (frameEvent.event === 'frame_removed') {
178
- emitter.emit('frameRemoved')
179
- } else if (frameEvent.event === 'notifications_enabled') {
180
- emitter.emit('notificationsEnabled', {
181
- notificationDetails: frameEvent.notificationDetails,
182
- })
183
- } else if (frameEvent.event === 'notifications_disabled') {
184
- emitter.emit('notificationsDisabled')
185
- } else if (frameEvent.event === 'back_navigation_triggered') {
186
- emitter.emit('backNavigationTriggered')
187
- }
188
- }
189
- }
190
- })
191
- }
package/src/sdkEmitter.ts DELETED
@@ -1,27 +0,0 @@
1
- import EventEmitter from 'eventemitter3'
2
- import type { Emitter, EventMap } from './types.ts'
3
-
4
- export function createEmitter(): Emitter {
5
- const emitter = new EventEmitter<EventMap>()
6
-
7
- return {
8
- get eventNames() {
9
- return emitter.eventNames.bind(emitter)
10
- },
11
- get listenerCount() {
12
- return emitter.listenerCount.bind(emitter)
13
- },
14
- get listeners() {
15
- return emitter.listeners.bind(emitter)
16
- },
17
- addListener: emitter.addListener.bind(emitter),
18
- emit: emitter.emit.bind(emitter),
19
- off: emitter.off.bind(emitter),
20
- on: emitter.on.bind(emitter),
21
- once: emitter.once.bind(emitter),
22
- removeAllListeners: emitter.removeAllListeners.bind(emitter),
23
- removeListener: emitter.removeListener.bind(emitter),
24
- }
25
- }
26
-
27
- export const emitter = createEmitter()
@@ -1,33 +0,0 @@
1
- import {
2
- type MiniAppHostCapability,
3
- type SolanaWalletProvider,
4
- type SolanaWireRequestFn,
5
- createSolanaWalletProvider,
6
- unwrapSolanaProviderRequest,
7
- } from '@farcaster/frame-core'
8
-
9
- import { frameHost } from './frameHost.ts'
10
-
11
- const { solanaProviderRequest } = frameHost
12
-
13
- let solanaProvider: SolanaWalletProvider | undefined
14
- if (solanaProviderRequest) {
15
- solanaProvider = createSolanaWalletProvider(
16
- unwrapSolanaProviderRequest(
17
- solanaProviderRequest as unknown as SolanaWireRequestFn,
18
- ),
19
- )
20
- }
21
-
22
- async function getSolanaProvider(): Promise<SolanaWalletProvider | undefined> {
23
- let capabilities: MiniAppHostCapability[] | undefined
24
- try {
25
- capabilities = await frameHost.getCapabilities()
26
- } catch {}
27
- if (!capabilities?.includes('wallet.getSolanaProvider')) {
28
- return undefined
29
- }
30
- return solanaProvider
31
- }
32
-
33
- export { getSolanaProvider }
package/src/types.ts DELETED
@@ -1,107 +0,0 @@
1
- import type {
2
- AddMiniApp,
3
- ComposeCast,
4
- Context,
5
- FrameNotificationDetails,
6
- GetCapabilities,
7
- GetChains,
8
- ImpactOccurred,
9
- NotificationOccurred,
10
- Ready,
11
- SelectionChanged,
12
- SendToken,
13
- SetPrimaryButtonOptions,
14
- SignIn,
15
- SolanaWalletProvider,
16
- SwapToken,
17
- ViewCast,
18
- ViewProfile,
19
- ViewToken,
20
- } from '@farcaster/frame-core'
21
- import type { EventEmitter } from 'eventemitter3'
22
- import type * as Provider from 'ox/Provider'
23
- import type { Back } from './back.ts'
24
- import type { QuickAuth } from './quickAuth.ts'
25
-
26
- declare global {
27
- interface Window {
28
- // Exposed by react-native-webview
29
- ReactNativeWebView: {
30
- postMessage: (message: string) => void
31
- }
32
- }
33
- }
34
-
35
- /** Combines members of an intersection into a readable type. */
36
- // https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=v01xkqU3KO0Mg
37
- type Compute<type> = { [key in keyof type]: type[key] } & unknown
38
-
39
- export type EventMap = {
40
- primaryButtonClicked: () => void
41
- frameAdded: ({
42
- notificationDetails,
43
- }: {
44
- notificationDetails?: FrameNotificationDetails
45
- }) => void
46
- frameAddRejected: ({
47
- reason,
48
- }: { reason: AddMiniApp.AddMiniAppRejectedReason }) => void
49
- frameRemoved: () => void
50
- notificationsEnabled: ({
51
- notificationDetails,
52
- }: {
53
- notificationDetails: FrameNotificationDetails
54
- }) => void
55
- notificationsDisabled: () => void
56
- backNavigationTriggered: () => void
57
- }
58
-
59
- export type Emitter = Compute<EventEmitter<EventMap>>
60
-
61
- type SetPrimaryButton = (options: SetPrimaryButtonOptions) => Promise<void>
62
-
63
- export type FrameSDK = {
64
- getCapabilities: GetCapabilities
65
- getChains: GetChains
66
- isInMiniApp: () => Promise<boolean>
67
- context: Promise<Context.FrameContext>
68
- back: Back
69
- quickAuth: QuickAuth
70
- actions: {
71
- ready: (options?: Partial<Ready.ReadyOptions>) => Promise<void>
72
- openUrl: (url: string) => Promise<void>
73
- close: () => Promise<void>
74
- setPrimaryButton: SetPrimaryButton
75
- // Deprecated in favor of addMiniApp
76
- addFrame: AddMiniApp.AddMiniApp
77
- addMiniApp: AddMiniApp.AddMiniApp
78
- signIn: SignIn.SignIn
79
- viewCast: ViewCast.ViewCast
80
- viewProfile: ViewProfile.ViewProfile
81
- composeCast: <close extends boolean | undefined = undefined>(
82
- options?: ComposeCast.Options<close>,
83
- ) => Promise<ComposeCast.Result<close>>
84
- viewToken: ViewToken.ViewToken
85
- sendToken: SendToken.SendToken
86
- swapToken: SwapToken.SwapToken
87
- }
88
- experimental: {
89
- getSolanaProvider: () => Promise<SolanaWalletProvider | undefined>
90
-
91
- /**
92
- * @deprecated - use `sdk.quickAuth.getToken`
93
- */
94
- quickAuth: QuickAuth['getToken']
95
- }
96
- wallet: {
97
- // Deprecated in favor of getEthereumProvider
98
- ethProvider: Provider.Provider
99
- getEthereumProvider: () => Promise<Provider.Provider | undefined>
100
- getSolanaProvider: () => Promise<SolanaWalletProvider | undefined>
101
- }
102
- haptics: {
103
- impactOccurred: ImpactOccurred
104
- notificationOccurred: NotificationOccurred
105
- selectionChanged: SelectionChanged
106
- }
107
- } & Emitter