├── CLAUDE.md
├── Examples
├── supabase
│ ├── seed.sql
│ ├── .gitignore
│ ├── functions
│ │ └── hello-world
│ │ │ └── index.ts
│ └── migrations
│ │ └── 20221223094509_init.sql
├── SlackClone
│ ├── supabase
│ │ ├── seed.sql
│ │ └── .gitignore
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── SlackClone.entitlements
│ ├── SlackCloneApp.swift
│ ├── Info.plist
│ ├── Logger.swift
│ ├── Supabase.swift
│ ├── Dependencies.swift
│ ├── ChannelListView.swift
│ ├── AuthView.swift
│ └── AppView.swift
├── UserManagement
│ ├── supabase
│ │ ├── seed.sql
│ │ └── .gitignore
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── supabase-swift-demo.png
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── UserManagement.entitlements
│ ├── UserManagementApp.swift
│ ├── SwiftUIHelpers.swift
│ ├── Models.swift
│ ├── Info.plist
│ ├── AppView.swift
│ ├── Supabase.swift
│ ├── AvatarImage.swift
│ └── AuthView.swift
├── Examples
│ ├── .ci
│ │ └── pre_build.sh
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Stringfy.swift
│ ├── Examples.entitlements
│ ├── Supabase.plist
│ ├── SupabaseConfig.swift
│ ├── UIApplicationExtensions.swift
│ ├── Constants.swift
│ ├── ErrorText.swift
│ ├── RootView.swift
│ ├── Debug.swift
│ ├── ThirdPartyAuthClerk.swift
│ ├── UIViewControllerWrapper.swift
│ ├── TodoListRow.swift
│ ├── Models.swift
│ ├── Auth
│ │ ├── AuthController.swift
│ │ ├── GoogleSignInSDKFlow.swift
│ │ └── SignInWithFacebook.swift
│ ├── AddTodoListView.swift
│ ├── Info.plist
│ ├── Shared
│ │ └── GitHubSourceLink.swift
│ ├── ActionState.swift
│ ├── Realtime
│ │ └── RealtimeExamplesView.swift
│ ├── Storage
│ │ ├── BucketList.swift
│ │ └── FileObjectDetailView.swift
│ └── HomeView.swift
├── Examples.xcodeproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Package.swift
├── .github
├── CODEOWNERS
├── copilot-instructions.md
├── workflows
│ ├── release.yml
│ └── conventional-commits.yml
├── dependabot.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.yml
│ └── bug_report.yml
├── .serena
└── .gitignore
├── Tests
├── StorageTests
│ ├── Fixtures
│ │ └── file.txt
│ ├── sadcat.jpg
│ ├── __Snapshots__
│ │ └── StorageBucketAPITests
│ │ │ └── StorageBucketAPITests-testCreateBucket.1.txt
│ ├── BucketOptionsTests.swift
│ ├── SupabaseStorageClient+Test.swift
│ ├── FileOptionsTests.swift
│ ├── StorageErrorTests.swift
│ └── TransformOptionsTests.swift
├── IntegrationTests
│ ├── supabase
│ │ ├── .temp
│ │ │ └── cli-latest
│ │ ├── .branches
│ │ │ └── _current_branch
│ │ └── .gitignore
│ ├── Fixtures
│ │ └── Upload
│ │ │ ├── file-2.txt
│ │ │ └── sadcat.jpg
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── settings.json
│ ├── DotEnv.swift
│ └── StorageClientIntegrationTests.swift
├── FunctionsTests
│ ├── __Snapshots__
│ │ └── RequestTests
│ │ │ ├── testInvokeWithCustomMethod.1.txt
│ │ │ ├── testInvokeWithDefaultOptions.1.txt
│ │ │ ├── testInvokeWithCustomHeader.1.txt
│ │ │ ├── testInvokeWithBody.1.txt
│ │ │ └── testInvokeWithCustomRegion.1.txt
│ ├── FunctionsErrorTests.swift
│ ├── FunctionInvokeOptionsTests.swift
│ └── RequestTests.swift
├── PostgRESTTests
│ ├── __Snapshots__
│ │ └── BuildURLRequestTests
│ │ │ ├── testBuildRequest.rpc-call-with-get.txt
│ │ │ ├── testBuildRequest.rpc-call-with-head.txt
│ │ │ ├── testBuildRequest.test-in-filter.txt
│ │ │ ├── testBuildRequest.query-if-nil-value.txt
│ │ │ ├── testBuildRequest.containedBy-using-array.txt
│ │ │ ├── testBuildRequest.containedBy-using-range.txt
│ │ │ ├── testBuildRequest.call-rpc-with-filter.txt
│ │ │ ├── testBuildRequest.call-rpc-without-parameter.txt
│ │ │ ├── testBuildRequest.containedBy-using-json.txt
│ │ │ ├── testBuildRequest.filter-starting-with-non-alphanumeric.txt
│ │ │ ├── testBuildRequest.filter-using-Date.txt
│ │ │ ├── testBuildRequest.call-rpc.txt
│ │ │ ├── testBuildRequest.iLikeAllOf.txt
│ │ │ ├── testBuildRequest.iLikeAnyOf.txt
│ │ │ ├── testBuildRequest.likeAllOf.txt
│ │ │ ├── testBuildRequest.likeAnyOf.txt
│ │ │ ├── testBuildRequest.query-non-default-schema.txt
│ │ │ ├── testBuildRequest.rpc-call-with-get-and-params.txt
│ │ │ ├── testBuildRequest.test-contains-filter-with-array.txt
│ │ │ ├── testBuildRequest.select-all-users-where-email-ends-with-supabase-co.txt
│ │ │ ├── testBuildRequest.test-contains-filter-with-dictionary.txt
│ │ │ ├── testBuildRequest.insert-new-user.txt
│ │ │ ├── testBuildRequest.test-or-filter-with-referenced-table.txt
│ │ │ ├── testBuildRequest.query-with-character.txt
│ │ │ ├── testBuildRequest.query-with-timestampz.txt
│ │ │ ├── testBuildRequest.select-after-an-insert.txt
│ │ │ ├── testBuildRequest.bulk-insert-users.txt
│ │ │ ├── testBuildRequest.test-upsert-ignoring-duplicates.txt
│ │ │ ├── testBuildRequest.test-upsert-not-ignoring-duplicates.txt
│ │ │ ├── testBuildRequest.bulk-upsert.txt
│ │ │ ├── testBuildRequest.select-after-bulk-upsert.txt
│ │ │ └── testBuildRequest.test-all-filters-and-count.txt
│ ├── JSONTests.swift
│ ├── PostgrestFilterValueTests.swift
│ ├── PostgresQueryTests.swift
│ └── PostgrestResponseTests.swift
├── AuthTests
│ ├── __Snapshots__
│ │ └── RequestsTests
│ │ │ ├── testSessionFromURL.1.txt
│ │ │ ├── testReauthenticate.1.txt
│ │ │ ├── testMFAUnenroll.1.txt
│ │ │ ├── testSignOut.1.txt
│ │ │ ├── testMFAChallenge.1.txt
│ │ │ ├── testSignOutWithLocalScope.1.txt
│ │ │ ├── testSignOutWithOthersScope.1.txt
│ │ │ ├── testUnlinkIdentity.1.txt
│ │ │ ├── testVerifyOTPUsingTokenHash.1.txt
│ │ │ ├── testRefreshSession.1.txt
│ │ │ ├── testDeleteUser.1.txt
│ │ │ ├── testSetSessionWithAExpiredToken.1.txt
│ │ │ ├── testMFAChallengePhone.1.txt
│ │ │ ├── testGetLinkIdentityURL.1.txt
│ │ │ ├── testSignInAnonymously.1.txt
│ │ │ ├── testResendPhone.1.txt
│ │ │ ├── testMFAVerify.1.txt
│ │ │ ├── testMFAEnrollLegacy.1.txt
│ │ │ ├── testMFAEnrollTotp.1.txt
│ │ │ ├── testResetPasswordForEmail.1.txt
│ │ │ ├── testMFAEnrollPhone.1.txt
│ │ │ ├── testVerifyOTPUsingPhone.1.txt
│ │ │ ├── testSignInWithSSOUsingDomain.1.txt
│ │ │ ├── testResendEmail.1.txt
│ │ │ ├── testSignInWithPhoneAndPassword.1.txt
│ │ │ ├── testSignInWithEmailAndPassword.1.txt
│ │ │ ├── testSignInWithSSOUsingProviderId.1.txt
│ │ │ ├── testVerifyOTPUsingEmail.1.txt
│ │ │ ├── testSignInWithOTPUsingPhone.1.txt
│ │ │ ├── testSignUpWithPhoneAndPassword.1.txt
│ │ │ ├── testSignInWithOTPUsingEmail.1.txt
│ │ │ ├── testSignUpWithEmailAndPassword.1.txt
│ │ │ ├── testSignInWithIdToken.1.txt
│ │ │ ├── testUpdateUser.1.txt
│ │ │ └── testSetSessionWithAFutureExpirationDate.1.txt
│ ├── AuthResponseTests.swift
│ ├── Mocks
│ │ └── Mocks.swift
│ ├── Resources
│ │ ├── signup-response.json
│ │ ├── anonymous-sign-in-response.json
│ │ ├── local-storage.json
│ │ ├── user.json
│ │ └── session.json
│ ├── MockHelpers.swift
│ ├── AuthClientMultipleInstancesTests.swift
│ ├── ExtractParamsTests.swift
│ └── PKCETests.swift
├── HelpersTests
│ ├── PostgrestErrorTests.swift
│ ├── JWTTests.swift
│ ├── WithTimeoutTests.swift
│ ├── ObservationTokenTests.swift
│ └── EventEmitterTests.swift
└── RealtimeTests
│ ├── RealtimePostgresFilterValueTests.swift
│ ├── ExportsTests.swift
│ ├── RealtimeErrorTests.swift
│ └── RealtimePostgresFilterTests.swift
├── .gitattributes
├── .release-please-manifest.json
├── supabase
├── .gitignore
├── migrations
│ └── 20240327182636_init_key_value_storage_schema.sql
└── seed.sql
├── Sources
├── Auth
│ ├── Exports.swift
│ ├── Storage
│ │ ├── AuthLocalStorage.swift
│ │ └── KeychainLocalStorage.swift
│ ├── AuthStateChangeListener.swift
│ ├── Internal
│ │ ├── JWTAlgorithm.swift
│ │ ├── URLOpener.swift
│ │ ├── EventEmitter.swift
│ │ ├── FixedWidthInteger+Random.swift
│ │ ├── Helpers.swift
│ │ ├── Dependencies.swift
│ │ ├── Constants.swift
│ │ ├── PKCE.swift
│ │ └── CodeVerifierStorage.swift
│ └── Defaults.swift
├── Realtime
│ ├── Exports.swift
│ ├── RealtimeError.swift
│ ├── PostgresActionData.swift
│ ├── RealtimePostgresFilterValue.swift
│ ├── RealtimePostgresFilter.swift
│ └── PushV2.swift
├── Storage
│ ├── Exports.swift
│ ├── StorageError.swift
│ ├── Codable.swift
│ ├── StorageHTTPClient.swift
│ ├── BucketOptions.swift
│ ├── SupabaseStorage.swift
│ └── TransformOptions.swift
├── Functions
│ └── Exports.swift
├── PostgREST
│ ├── Exports.swift
│ └── Defaults.swift
├── TestHelpers
│ ├── Exports.swift
│ ├── AsyncSequence.swift
│ ├── WithMainSerialExecutor+Windows.swift
│ ├── InMemoryLocalStorage.swift
│ ├── MockExtensions.swift
│ └── HTTPClientMock.swift
├── Supabase
│ ├── Exports.swift
│ ├── Constants.swift
│ └── Deprecated.swift
└── Helpers
│ ├── SharedModels
│ ├── PostgrestError.swift
│ └── HTTPError.swift
│ ├── HTTP
│ ├── HTTPResponse.swift
│ ├── HTTPClient.swift
│ ├── LoggerInterceptor.swift
│ └── HTTPFields.swift
│ ├── Task+withTimeout.swift
│ ├── Base64URL.swift
│ ├── Codable.swift
│ ├── TaskLocalHelpers.swift
│ ├── _Clock.swift
│ ├── Logger
│ └── OSLogSupabaseLogger.swift
│ ├── JWT.swift
│ └── Version.swift
├── scripts
├── run-on-linux.sh
├── load_env.sh
└── check-for-breaking-api-changes.sh
├── .editorconfig
├── .spi.yml
├── .env.example
├── .vscode
└── settings.json
├── Supabase.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── release-please-config.json
└── LICENSE
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | @AGENTS.md
2 |
--------------------------------------------------------------------------------
/Examples/supabase/seed.sql:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @grdsdev
2 |
--------------------------------------------------------------------------------
/.serena/.gitignore:
--------------------------------------------------------------------------------
1 | /cache
2 |
--------------------------------------------------------------------------------
/Examples/SlackClone/supabase/seed.sql:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Examples/UserManagement/supabase/seed.sql:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tests/StorageTests/Fixtures/file.txt:
--------------------------------------------------------------------------------
1 | hello world!
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /Tests/**/__Snapshots__/**/*.txt eol=lf
2 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "2.38.1"
3 | }
--------------------------------------------------------------------------------
/Tests/IntegrationTests/supabase/.temp/cli-latest:
--------------------------------------------------------------------------------
1 | v2.22.12
--------------------------------------------------------------------------------
/Tests/IntegrationTests/supabase/.branches/_current_branch:
--------------------------------------------------------------------------------
1 | main
--------------------------------------------------------------------------------
/Examples/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/Fixtures/Upload/file-2.txt:
--------------------------------------------------------------------------------
1 | supabase txt file 2
2 |
--------------------------------------------------------------------------------
/Examples/Examples/.ci/pre_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cp _Secrets.swift Secrets.swift
--------------------------------------------------------------------------------
/Examples/SlackClone/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/Examples/UserManagement/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/Tests/StorageTests/sadcat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase/supabase-swift/HEAD/Tests/StorageTests/sadcat.jpg
--------------------------------------------------------------------------------
/Tests/IntegrationTests/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "denoland.vscode-deno"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/Examples/Examples/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/SlackClone/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/UserManagement/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
5 | # dotenvx
6 | .env.keys
7 | .env.local
8 | .env.*.local
9 |
--------------------------------------------------------------------------------
/Examples/UserManagement/supabase-swift-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase/supabase-swift/HEAD/Examples/UserManagement/supabase-swift-demo.png
--------------------------------------------------------------------------------
/Examples/Examples/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase/supabase-swift/HEAD/Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg
--------------------------------------------------------------------------------
/Examples/SlackClone/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/UserManagement/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/Auth/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Helpers
9 |
--------------------------------------------------------------------------------
/Sources/Realtime/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Helpers
9 |
--------------------------------------------------------------------------------
/Sources/Storage/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Helpers
9 |
--------------------------------------------------------------------------------
/Sources/Functions/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Helpers
9 |
--------------------------------------------------------------------------------
/Sources/PostgREST/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Helpers
9 |
--------------------------------------------------------------------------------
/Sources/TestHelpers/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Helpers
9 |
--------------------------------------------------------------------------------
/scripts/run-on-linux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | SWIFT_VERSION="latest"
4 |
5 | # Spin Swift Docker container
6 | docker run -it --rm -v $(pwd):/app -w /app "swift:$SWIFT_VERSION" bash
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | ident_style = space
7 | ident_size = 2
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 |
--------------------------------------------------------------------------------
/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/Examples/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/SlackClone/SlackClone.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - platform: ios
5 | documentation_targets:
6 | - Supabase
7 | - Auth
8 | - Storage
9 | - PostgREST
10 | - Realtime
11 | - Functions
--------------------------------------------------------------------------------
/Examples/SlackClone/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/UserManagement/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/UserManagement/UserManagement.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomMethod.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request PATCH \
3 | --header "apikey: supabase.anon.key" \
4 | --header "x-client-info: functions-swift/x.y.z" \
5 | "http://localhost:5432/functions/v1/hello-world"
--------------------------------------------------------------------------------
/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithDefaultOptions.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "apikey: supabase.anon.key" \
4 | --header "x-client-info: functions-swift/x.y.z" \
5 | "http://localhost:5432/functions/v1/hello-world"
--------------------------------------------------------------------------------
/supabase/migrations/20240327182636_init_key_value_storage_schema.sql:
--------------------------------------------------------------------------------
1 | create table key_value_storage(
2 | "key" text primary key,
3 | "value" jsonb not null
4 | );
5 |
6 | alter publication supabase_realtime
7 | add table key_value_storage;
8 |
9 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Get these from your API settings: https://supabase.com/dashboard/project/_/settings/api
2 |
3 | SUPABASE_URL=https://mysupabasereference.supabase.co
4 | SUPABASE_ANON_KEY=my.supabase.anon.key
5 | SUPABASE_SERVICE_ROLE_KEY=my.supabase.service.role.key
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "apikey",
4 | "HTTPURL",
5 | "pkce",
6 | "postgrest",
7 | "preconcurrency",
8 | "Supabase",
9 | "whitespaces",
10 | "xctest"
11 | ],
12 | "makefile.configureOnOpen": false
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/rpc/sum"
--------------------------------------------------------------------------------
/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomHeader.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "apikey: supabase.anon.key" \
4 | --header "x-client-info: functions-swift/x.y.z" \
5 | --header "x-custom-key: custom value" \
6 | "http://localhost:5432/functions/v1/hello-world"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-head.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --head \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | "https://example.supabase.co/rpc/sum"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-in-filter.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/todos?id=in.(1,2,3)&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-if-nil-value.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?email=is.NULL&select=*"
--------------------------------------------------------------------------------
/Supabase.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Apikey: dummy.api.key" \
3 | --header "Authorization: bearer accesstoken" \
4 | --header "X-Client-Info: gotrue-swift/x.y.z" \
5 | --header "X-Supabase-Api-Version: 2024-01-01" \
6 | "http://localhost:54321/auth/v1/user"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.containedBy-using-array.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?id=cd.%7Ba,b,c%7D&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.containedBy-using-range.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?age=cd.%5B10,20%5D&select=*"
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | ".": {
4 | "release-type": "simple",
5 | "extra-files": [
6 | "Sources/Helpers/Version.swift"
7 | ]
8 | }
9 | },
10 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
11 | }
--------------------------------------------------------------------------------
/Supabase.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testReauthenticate.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Apikey: dummy.api.key" \
3 | --header "Authorization: Bearer accesstoken" \
4 | --header "X-Client-Info: gotrue-swift/x.y.z" \
5 | --header "X-Supabase-Api-Version: 2024-01-01" \
6 | "http://localhost:54321/auth/v1/reauthenticate"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.call-rpc-with-filter.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | "https://example.supabase.co/rpc/test_fcn?id=eq.1"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.call-rpc-without-parameter.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | "https://example.supabase.co/rpc/test_fcn"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.containedBy-using-json.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?select=*&userMetadata=cd.%7B%22age%22:18%7D"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.filter-starting-with-non-alphanumeric.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?select=*&to=eq.+16505555555"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.filter-using-Date.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?created_at=gt.1970-01-01T00:00:00.000Z&select=*"
--------------------------------------------------------------------------------
/Examples/Examples/Stringfy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Stringfy.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 21/03/24.
6 | //
7 |
8 | import CustomDump
9 | import Foundation
10 |
11 | func stringfy(_ value: Any) -> String {
12 | var output = ""
13 | customDump(value, to: &output)
14 | return output
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Supabase/Exports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exports.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 30/05/25.
6 | //
7 |
8 | @_exported import Auth
9 | @_exported import Functions
10 | @_exported import Helpers
11 | @_exported import PostgREST
12 | @_exported import Realtime
13 | @_exported import Storage
14 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAUnenroll.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request DELETE \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | "http://localhost:54321/auth/v1/factors/123"
--------------------------------------------------------------------------------
/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithBody.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Content-Type: application/json" \
4 | --header "apikey: supabase.anon.key" \
5 | --header "x-client-info: functions-swift/x.y.z" \
6 | --data "{\"name\":\"Supabase\"}" \
7 | "http://localhost:5432/functions/v1/hello-world"
--------------------------------------------------------------------------------
/Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomRegion.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "apikey: supabase.anon.key" \
4 | --header "x-client-info: functions-swift/x.y.z" \
5 | --header "x-region: ap-northeast-1" \
6 | "http://localhost:5432/functions/v1/hello-world?forceFunctionRegion=ap-northeast-1"
--------------------------------------------------------------------------------
/Tests/StorageTests/__Snapshots__/StorageBucketAPITests/StorageBucketAPITests-testCreateBucket.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: storage-swift/2.24.4" \
5 | --data "{\"public\":true,\"id\":\"newbucket\",\"name\":\"newbucket\"}" \
6 | "http://example.com/bucket"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | "http://localhost:54321/auth/v1/logout?scope=global"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.call-rpc.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | --data "{\"KEY\":\"VALUE\"}" \
7 | "https://example.supabase.co/rpc/test_fcn"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.iLikeAllOf.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?email=ilike(all).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.iLikeAnyOf.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?email=ilike(any).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.likeAllOf.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?email=like(all).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.likeAnyOf.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?email=like(any).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-non-default-schema.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Accept-Profile: storage" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | "https://example.supabase.co/objects?select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get-and-params.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/rpc/get_array_element?array=%7B37,420,64%7D&index=2"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-contains-filter-with-array.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?name=cs.%7Bis:online,faction:red%7D&select=*"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallenge.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | "http://localhost:54321/auth/v1/factors/123/challenge"
--------------------------------------------------------------------------------
/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/TestHelpers/AsyncSequence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncSequence.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 04/04/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension AsyncSequence {
11 | package func collect() async rethrows -> [Element] {
12 | try await reduce(into: [Element]()) { $0.append($1) }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | "http://localhost:54321/auth/v1/logout?scope=local"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.select-all-users-where-email-ends-with-supabase-co.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?email=like.%25@supabase.co&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-contains-filter-with-dictionary.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?address=cs.%7B%22postcode%22:90210%7D&select=name"
--------------------------------------------------------------------------------
/Examples/UserManagement/UserManagementApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserManagementApp.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct UserManagementApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | AppView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | "http://localhost:54321/auth/v1/logout?scope=others"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.insert-new-user.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | --data "{\"email\":\"johndoe@supabase.io\"}" \
7 | "https://example.supabase.co/users"
--------------------------------------------------------------------------------
/Examples/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import Foundation
5 | import PackageDescription
6 |
7 | var package = Package(
8 | name: "Examples",
9 | platforms: [],
10 | products: [],
11 | dependencies: [],
12 | targets: []
13 | )
14 |
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-or-filter-with-referenced-table.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?messages.or=(public.eq.true,recipient_id.eq.1)&select=*,messages(*)"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-character.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/users?id=eq.Cig%C3%A1nyka-%C3%A9r%20(0+400%20cskm)%20v%C3%ADzrajzi%20%C3%A1llom%C3%A1s&select=*"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-timestampz.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/tasks?order=received_at.asc.nullslast&received_at=gt.2023-03-23T15:50:30.511743+00:00&select=*"
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # Copilot Instructions for supabase-swift
2 |
3 | ## Instructions for Code Review
4 |
5 | - Check for breaking API changes
6 | - Check if documentation is up to date
7 | - Check if examples are up to date
8 | - Check if tests are comprehensive and cover all critical paths
9 |
10 | ## Repository Overview
11 |
12 | @../AGENTS.md
13 |
14 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testUnlinkIdentity.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request DELETE \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | "http://localhost:54321/auth/v1/user/identities/E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingTokenHash.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"token_hash\":\"abc-def\",\"type\":\"email\"}" \
8 | "http://localhost:54321/auth/v1/verify"
--------------------------------------------------------------------------------
/Examples/UserManagement/SwiftUIHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIHelpers.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | func onMac(_ block: (Self) -> some View) -> some View {
12 | #if os(macOS)
13 | return block(self)
14 | #else
15 | return self
16 | #endif
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"refresh_token\":\"refresh-token\"}" \
8 | "http://localhost:54321/auth/v1/token?grant_type=refresh_token"
--------------------------------------------------------------------------------
/Examples/Examples/Examples.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.applesignin
6 |
7 | Default
8 |
9 | keychain-access-groups
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testDeleteUser.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request DELETE \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"should_soft_delete\":false}" \
8 | "http://localhost:54321/auth/v1/admin/users/E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
--------------------------------------------------------------------------------
/Examples/Examples/Supabase.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SUPABASE_URL
6 | http://127.0.0.1:54321
7 | SUPABASE_ANON_KEY
8 | sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"refresh_token\":\"dummy-refresh-token\"}" \
8 | "http://localhost:54321/auth/v1/token?grant_type=refresh_token"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.select-after-an-insert.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "Prefer: return=representation" \
6 | --header "X-Client-Info: postgrest-swift/x.y.z" \
7 | --data "{\"email\":\"johndoe@supabase.io\"}" \
8 | "https://example.supabase.co/users?select=id,email"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallengePhone.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "Content-Type: application/json" \
6 | --header "X-Client-Info: gotrue-swift/x.y.z" \
7 | --header "X-Supabase-Api-Version: 2024-01-01" \
8 | --data "{\"channel\":\"whatsapp\"}" \
9 | "http://localhost:54321/auth/v1/factors/123/challenge"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testGetLinkIdentityURL.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Apikey: dummy.api.key" \
3 | --header "Authorization: Bearer accesstoken" \
4 | --header "X-Client-Info: gotrue-swift/x.y.z" \
5 | --header "X-Supabase-Api-Version: 2024-01-01" \
6 | "http://localhost:54321/auth/v1/user/identities/authorize?extra_key=extra_value&provider=github&redirect_to=https://supabase.com&scopes=user:email&skip_http_redirect=true"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.bulk-insert-users.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: postgrest-swift/x.y.z" \
6 | --data "[{\"email\":\"johndoe@supabase.io\"},{\"email\":\"johndoe2@supabase.io\",\"username\":\"johndoe2\"}]" \
7 | "https://example.supabase.co/users?columns=email,username"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-upsert-ignoring-duplicates.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "Prefer: resolution=ignore-duplicates,return=representation" \
6 | --header "X-Client-Info: postgrest-swift/x.y.z" \
7 | --data "{\"email\":\"johndoe@supabase.io\"}" \
8 | "https://example.supabase.co/users"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-upsert-not-ignoring-duplicates.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "Prefer: resolution=merge-duplicates,return=representation" \
6 | --header "X-Client-Info: postgrest-swift/x.y.z" \
7 | --data "{\"email\":\"johndoe@supabase.io\"}" \
8 | "https://example.supabase.co/users"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInAnonymously.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \
8 | "http://localhost:54321/auth/v1/signup"
--------------------------------------------------------------------------------
/Sources/TestHelpers/WithMainSerialExecutor+Windows.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WithMainSerialExecutor+Windows.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 12/03/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if os(Windows)
11 | /// Calling this method on Windows has no effect.
12 | public func withMainSerialExecutor(
13 | @_implicitSelfCapture operation: () throws -> Void
14 | ) rethrows {
15 | try operation()
16 | }
17 | #endif
18 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"type\":\"phone_change\"}" \
8 | "http://localhost:54321/auth/v1/resend"
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - release/*
8 | workflow_dispatch:
9 |
10 | permissions:
11 | contents: write
12 | pull-requests: write
13 |
14 | jobs:
15 | release-please:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: googleapis/release-please-action@v4
19 | id: release
20 | with:
21 | target-branch: ${{ github.ref_name }}
22 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAVerify.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "Content-Type: application/json" \
6 | --header "X-Client-Info: gotrue-swift/x.y.z" \
7 | --header "X-Supabase-Api-Version: 2024-01-01" \
8 | --data "{\"challenge_id\":\"123\",\"code\":\"123456\",\"factor_id\":\"123\"}" \
9 | "http://localhost:54321/auth/v1/factors/123/verify"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollLegacy.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "Content-Type: application/json" \
6 | --header "X-Client-Info: gotrue-swift/x.y.z" \
7 | --header "X-Supabase-Api-Version: 2024-01-01" \
8 | --data "{\"factor_type\":\"totp\",\"friendly_name\":\"test\",\"issuer\":\"supabase.com\"}" \
9 | "http://localhost:54321/auth/v1/factors"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollTotp.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "Content-Type: application/json" \
6 | --header "X-Client-Info: gotrue-swift/x.y.z" \
7 | --header "X-Supabase-Api-Version: 2024-01-01" \
8 | --data "{\"factor_type\":\"totp\",\"friendly_name\":\"test\",\"issuer\":\"supabase.com\"}" \
9 | "http://localhost:54321/auth/v1/factors"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \
8 | "http://localhost:54321/auth/v1/recover?redirect_to=https://supabase.com"
--------------------------------------------------------------------------------
/scripts/load_env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | env_file="${1:-.env}"
4 |
5 | if [ ! -f "$env_file" ]; then
6 | echo "Error: Environment file '$env_file' not found." >&2
7 | exit 1
8 | fi
9 |
10 | while IFS= read -r line; do
11 | line="$(echo "${line%%#*}" | xargs)"
12 | if [ -n "$line" ]; then
13 | export "$line" || {
14 | echo "Error exporting: $line" >&2
15 | exit 1
16 | }
17 | fi
18 | done <"$env_file"
19 |
--------------------------------------------------------------------------------
/Examples/SlackClone/SlackCloneApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlackCloneApp.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 27/12/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | @MainActor
12 | struct SlackCloneApp: App {
13 | let model = AppViewModel()
14 |
15 | var body: some Scene {
16 | WindowGroup {
17 | AppView(model: model)
18 | .onOpenURL { url in
19 | supabase.handle(url)
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollPhone.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "Content-Type: application/json" \
6 | --header "X-Client-Info: gotrue-swift/x.y.z" \
7 | --header "X-Supabase-Api-Version: 2024-01-01" \
8 | --data "{\"factor_type\":\"phone\",\"friendly_name\":\"test\",\"phone\":\"+1 202-918-2132\"}" \
9 | "http://localhost:54321/auth/v1/factors"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"token\":\"123456\",\"type\":\"sms\"}" \
8 | "http://localhost:54321/auth/v1/verify"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"domain\":\"supabase.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"redirect_to\":\"https:\/\/supabase.com\"}" \
8 | "http://localhost:54321/auth/v1/sso"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"type\":\"email_change\"}" \
8 | "http://localhost:54321/auth/v1/resend?redirect_to=https://supabase.com"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \
8 | "http://localhost:54321/auth/v1/token?grant_type=password"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \
8 | "http://localhost:54321/auth/v1/token?grant_type=password"
--------------------------------------------------------------------------------
/Tests/HelpersTests/PostgrestErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgrestErrorTests.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 07/05/24.
6 | //
7 |
8 | import Foundation
9 | import Helpers
10 | import XCTest
11 |
12 | final class PostgrestErrorTests: XCTestCase {
13 |
14 | func testLocalizedErrorConformance() {
15 | let error = PostgrestError(message: "test error message")
16 | XCTAssertEqual(error.errorDescription, "test error message")
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.bulk-upsert.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "Prefer: resolution=merge-duplicates,return=representation" \
6 | --header "X-Client-Info: postgrest-swift/x.y.z" \
7 | --data "[{\"email\":\"johndoe@supabase.io\"},{\"email\":\"johndoe2@supabase.io\",\"username\":\"johndoe2\"}]" \
8 | "https://example.supabase.co/users?columns=email,username"
--------------------------------------------------------------------------------
/Examples/Examples/SupabaseConfig.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum SupabaseConfig {
4 | static subscript(key: String) -> String? {
5 | guard let plistFileURL = Bundle.main.url(forResource: "Supabase", withExtension: "plist"),
6 | let plistData = try? Data(contentsOf: plistFileURL),
7 | let plist = try? PropertyListSerialization.propertyList(from: plistData, format: nil)
8 | as? [String: Any]
9 | else { return nil }
10 |
11 | return plist[key] as? String
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Examples/UserManagement/Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Models.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Profile: Codable {
11 | let username: String?
12 | let fullName: String?
13 | let website: String?
14 | let avatarURL: String?
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case username
18 | case fullName = "full_name"
19 | case website
20 | case avatarURL = "avatar_url"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"provider_id\":\"E621E1F8-C36C-495A-93FC-0C247A3E6E5F\",\"redirect_to\":\"https:\/\/supabase.com\"}" \
8 | "http://localhost:54321/auth/v1/sso"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.select-after-bulk-upsert.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Accept: application/json" \
4 | --header "Content-Type: application/json" \
5 | --header "Prefer: resolution=merge-duplicates,return=representation" \
6 | --header "X-Client-Info: postgrest-swift/x.y.z" \
7 | --data "[{\"email\":\"johndoe@supabase.io\"},{\"email\":\"johndoe2@supabase.io\"}]" \
8 | "https://example.supabase.co/users?columns=email&on_conflict=username&select=*"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"token\":\"123456\",\"type\":\"magiclink\"}" \
8 | "http://localhost:54321/auth/v1/verify?redirect_to=https://supabase.com"
--------------------------------------------------------------------------------
/Examples/UserManagement/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLSchemes
11 |
12 | io.supabase.user-management
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"channel\":\"sms\",\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"phone\":\"+1 202-918-2132\"}" \
8 | "http://localhost:54321/auth/v1/otp"
--------------------------------------------------------------------------------
/Tests/IntegrationTests/DotEnv.swift:
--------------------------------------------------------------------------------
1 | enum DotEnv {
2 | static let SUPABASE_URL = "http://127.0.0.1:54321"
3 | static let SUPABASE_ANON_KEY =
4 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
5 | static let SUPABASE_SERVICE_ROLE_KEY =
6 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
7 | }
8 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"channel\":\"sms\",\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \
8 | "http://localhost:54321/auth/v1/signup"
--------------------------------------------------------------------------------
/Examples/Examples/UIApplicationExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIApplicationExtensions.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 05/03/24.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import UIKit
10 |
11 | extension UIApplication {
12 | var firstKeyWindow: UIWindow? {
13 | UIApplication.shared
14 | .connectedScenes
15 | .compactMap { $0 as? UIWindowScene }
16 | .filter { $0.activationState == .foregroundActive }
17 | .first?.keyWindow
18 | }
19 | }
20 | #endif
21 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"}}" \
8 | "http://localhost:54321/auth/v1/otp?redirect_to=https://supabase.com"
--------------------------------------------------------------------------------
/Tests/IntegrationTests/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[typescript]": {
3 | "editor.defaultFormatter": "denoland.vscode-deno"
4 | },
5 | "deno.enablePaths": [
6 | "supabase/functions"
7 | ],
8 | "deno.lint": true,
9 | "deno.unstable": [
10 | "bare-node-builtins",
11 | "byonm",
12 | "sloppy-imports",
13 | "unsafe-proto",
14 | "webgpu",
15 | "broadcast-channel",
16 | "worker-options",
17 | "cron",
18 | "kv",
19 | "ffi",
20 | "fs",
21 | "http",
22 | "net"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/Examples/Examples/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 15/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Constants {
11 | static let redirectToURL = URL(string: "com.supabase.swift-examples://")!
12 | }
13 |
14 | extension URL {
15 | init?(scheme: String) {
16 | var components = URLComponents()
17 | components.scheme = scheme
18 |
19 | guard let url = components.url else {
20 | return nil
21 | }
22 |
23 | self = url
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \
8 | "http://localhost:54321/auth/v1/signup?redirect_to=https://supabase.com"
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request POST \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Content-Type: application/json" \
5 | --header "X-Client-Info: gotrue-swift/x.y.z" \
6 | --header "X-Supabase-Api-Version: 2024-01-01" \
7 | --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"link_identity\":false,\"nonce\":\"nonce\",\"provider\":\"apple\"}" \
8 | "http://localhost:54321/auth/v1/token?grant_type=id_token"
--------------------------------------------------------------------------------
/Sources/Storage/StorageError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct StorageError: Error, Decodable, Sendable {
4 | public var statusCode: String?
5 | public var message: String
6 | public var error: String?
7 |
8 | public init(statusCode: String? = nil, message: String, error: String? = nil) {
9 | self.statusCode = statusCode
10 | self.message = message
11 | self.error = error
12 | }
13 | }
14 |
15 | extension StorageError: LocalizedError {
16 | public var errorDescription: String? {
17 | message
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Supabase/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 06/03/25.
6 | //
7 |
8 | import Foundation
9 |
10 | let defaultHeaders: [String: String] = {
11 | var headers = [
12 | "X-Client-Info": "supabase-swift/\(version)"
13 | ]
14 |
15 | if let platform {
16 | headers["X-Supabase-Client-Platform"] = platform
17 | }
18 |
19 | if let platformVersion {
20 | headers["X-Supabase-Client-Platform-Version"] = platformVersion
21 | }
22 |
23 | return headers
24 | }()
25 |
--------------------------------------------------------------------------------
/Sources/Realtime/RealtimeError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimeError.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 30/10/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RealtimeError: LocalizedError {
11 | var errorDescription: String?
12 |
13 | init(_ errorDescription: String) {
14 | self.errorDescription = errorDescription
15 | }
16 | }
17 |
18 | extension RealtimeError {
19 | /// The maximum retry attempts reached.
20 | static var maxRetryAttemptsReached: Self {
21 | Self("Maximum retry attempts reached.")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --request PUT \
3 | --header "Apikey: dummy.api.key" \
4 | --header "Authorization: Bearer accesstoken" \
5 | --header "Content-Type: application/json" \
6 | --header "X-Client-Info: gotrue-swift/x.y.z" \
7 | --header "X-Supabase-Api-Version: 2024-01-01" \
8 | --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"email_change_token\":\"123456\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \
9 | "http://localhost:54321/auth/v1/user"
--------------------------------------------------------------------------------
/Examples/Examples/ErrorText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorText.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 23/12/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ErrorText: View {
11 | let error: Error
12 |
13 | init(_ error: Error) {
14 | self.error = error
15 | }
16 |
17 | var body: some View {
18 | Text(error.localizedDescription)
19 | .foregroundColor(.red)
20 | .font(.footnote)
21 | }
22 | }
23 |
24 | struct ErrorText_Previews: PreviewProvider {
25 | static var previews: some View {
26 | ErrorText(NSError())
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Auth/Storage/AuthLocalStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol AuthLocalStorage: Sendable {
4 | func store(key: String, value: Data) throws
5 | func retrieve(key: String) throws -> Data?
6 | func remove(key: String) throws
7 | }
8 |
9 | extension AuthClient.Configuration {
10 | #if !os(Linux) && !os(Windows) && !os(Android)
11 | public static let defaultLocalStorage: any AuthLocalStorage = KeychainLocalStorage()
12 | #elseif os(Windows)
13 | public static let defaultLocalStorage: any AuthLocalStorage = WinCredLocalStorage()
14 | #endif
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Realtime/PostgresActionData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgresActionData.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 26/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct PostgresActionData: Codable {
11 | var type: String
12 | var record: [String: AnyJSON]?
13 | var oldRecord: [String: AnyJSON]?
14 | var columns: [Column]
15 | var commitTimestamp: Date
16 |
17 | enum CodingKeys: String, CodingKey {
18 | case type
19 | case record
20 | case oldRecord = "old_record"
21 | case columns
22 | case commitTimestamp = "commit_timestamp"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Examples/Examples/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 22/12/22.
6 | //
7 |
8 | import Auth
9 | import SwiftUI
10 |
11 | struct RootView: View {
12 | @Environment(AuthController.self) var auth
13 |
14 | var body: some View {
15 | if auth.session == nil {
16 | NavigationStack {
17 | AuthExamplesView()
18 | }
19 | } else {
20 | HomeView()
21 | }
22 | }
23 | }
24 |
25 | struct ContentView_Previews: PreviewProvider {
26 | static var previews: some View {
27 | RootView()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Examples/Examples/Debug.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Debug.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 15/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | func debug(
11 | _ message: @autoclosure () -> String,
12 | function: String = #function,
13 | file: String = #file,
14 | line: UInt = #line
15 | ) {
16 | assert(
17 | {
18 | let fileHandle = FileHandle.standardError
19 |
20 | let logLine = "[\(function) \(file.split(separator: "/").last!):\(line)] \(message())\n"
21 | fileHandle.write(Data(logLine.utf8))
22 |
23 | return true
24 | }()
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/FunctionsTests/FunctionsErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FunctionsErrorTests.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 20/01/25.
6 | //
7 |
8 | import Supabase
9 | import XCTest
10 |
11 | final class FunctionsErrorTests: XCTestCase {
12 |
13 | func testLocalizedDescription() {
14 | XCTAssertEqual(
15 | FunctionsError.relayError.localizedDescription, "Relay Error invoking the Edge Function")
16 | XCTAssertEqual(
17 | FunctionsError.httpError(code: 412, data: Data()).localizedDescription,
18 | "Edge Function returned a non-2xx status code: 412")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Examples/SlackClone/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLIconFile
11 |
12 | CFBundleURLName
13 | com.supabase.slack-clone
14 | CFBundleURLSchemes
15 |
16 | com.supabase.slack-clone
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Tests/RealtimeTests/RealtimePostgresFilterValueTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimePostgresFilterValueTests.swift
3 | // Supabase
4 | //
5 | // Created by Lucas Abijmil on 19/02/2025.
6 | //
7 |
8 | import XCTest
9 | @testable import Realtime
10 |
11 | final class RealtimePostgresFilterValueTests: XCTestCase {
12 | func testUUID() {
13 | XCTAssertEqual(
14 | UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!.rawValue,
15 | "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")
16 | }
17 |
18 | func testDate() {
19 | XCTAssertEqual(
20 | Date(timeIntervalSince1970: 1_737_465_985).rawValue,
21 | "2025-01-21T13:26:25.000Z"
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Examples/Examples/ThirdPartyAuthClerk.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThirdPartyAuthClerk.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 26/03/25.
6 | //
7 |
8 | import Clerk
9 | import Foundation
10 | import Supabase
11 |
12 | extension SupabaseClient {
13 | static let thirdPartyAuthWithClerk = SupabaseClient(
14 | supabaseURL: URL(string: SupabaseConfig["SUPABASE_URL"]!)!,
15 | supabaseKey: SupabaseConfig["SUPABASE_ANON_KEY"]!,
16 | options: SupabaseClientOptions(
17 | auth: SupabaseClientOptions.AuthOptions(
18 | accessToken: {
19 | try await Clerk.shared.session?.getToken()?.jwt
20 | }
21 | )
22 | )
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/AuthTests/AuthResponseTests.swift:
--------------------------------------------------------------------------------
1 | import Auth
2 | import SnapshotTesting
3 | import XCTest
4 |
5 | final class AuthResponseTests: XCTestCase {
6 | func testSession() throws {
7 | let response = try AuthClient.Configuration.jsonDecoder.decode(
8 | AuthResponse.self,
9 | from: json(named: "session")
10 | )
11 | XCTAssertNotNil(response.session)
12 | XCTAssertEqual(response.user, response.session?.user)
13 | }
14 |
15 | func testUser() throws {
16 | let response = try AuthClient.Configuration.jsonDecoder.decode(
17 | AuthResponse.self,
18 | from: json(named: "user")
19 | )
20 | XCTAssertNil(response.session)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Apikey: dummy.api.key" \
3 | --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjo0ODUyMTYzNTkzLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.UiEhoahP9GNrBKw_OHBWyqYudtoIlZGkrjs7Qa8hU7I" \
4 | --header "X-Client-Info: gotrue-swift/x.y.z" \
5 | --header "X-Supabase-Api-Version: 2024-01-01" \
6 | "http://localhost:54321/auth/v1/user"
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/JSONTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 01/07/24.
6 | //
7 |
8 | @testable import PostgREST
9 | import XCTest
10 |
11 | final class JSONTests: XCTestCase {
12 | func testDecodeJSON() throws {
13 | let json = """
14 | {
15 | "created_at": "2024-06-15T18:12:04+00:00"
16 | }
17 | """.data(using: .utf8)!
18 |
19 | struct Value: Decodable {
20 | var createdAt: Date
21 |
22 | enum CodingKeys: String, CodingKey {
23 | case createdAt = "created_at"
24 | }
25 | }
26 | _ = try PostgrestClient.Configuration.jsonDecoder.decode(Value.self, from: json)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/HelpersTests/JWTTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Helpers
3 |
4 | final class JWTTests: XCTestCase {
5 | func testDecodeJWT() throws {
6 | let token =
7 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.CGr5zNE5Yltlbn_3Ms2cjSLs_AW9RKM3lxh7cTQrg0w"
8 | let jwt = JWT.decodePayload(token)
9 | let exp = try XCTUnwrap(jwt?["exp"] as? TimeInterval)
10 | XCTAssertEqual(exp, 1_648_640_021)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "swift"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | - package-ecosystem: "npm"
13 | directory: "/"
14 | schedule:
15 | interval: "weekly"
16 | - package-ecosystem: "github-actions"
17 | directory: "/"
18 | schedule:
19 | interval: "weekly"
20 |
--------------------------------------------------------------------------------
/Examples/UserManagement/AppView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppView.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AppView: View {
11 | @State var isAuthenticated = false
12 |
13 | var body: some View {
14 | Group {
15 | if isAuthenticated {
16 | ProfileView()
17 | } else {
18 | AuthView()
19 | }
20 | }
21 | .task {
22 | for await state in supabase.auth.authStateChanges {
23 | if [.initialSession, .signedIn, .signedOut].contains(state.event) {
24 | isAuthenticated = state.session != nil
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
31 | #Preview {
32 | AppView()
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/TestHelpers/InMemoryLocalStorage.swift:
--------------------------------------------------------------------------------
1 | import Auth
2 | import ConcurrencyExtras
3 | import Foundation
4 |
5 | package final class InMemoryLocalStorage: AuthLocalStorage, @unchecked Sendable {
6 | let _storage = LockIsolated([String: Data]())
7 |
8 | package var storage: [String: Data] {
9 | _storage.value
10 | }
11 |
12 | package init() {}
13 |
14 | package func store(key: String, value: Data) throws {
15 | _storage.withValue {
16 | $0[key] = value
17 | }
18 | }
19 |
20 | package func retrieve(key: String) throws -> Data? {
21 | _storage.value[key]
22 | }
23 |
24 | package func remove(key: String) throws {
25 | _storage.withValue {
26 | $0[key] = nil
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Supabase/Deprecated.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Deprecated.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 15/05/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SupabaseClient {
11 | /// Database client for Supabase.
12 | @available(
13 | *,
14 | deprecated,
15 | message: "Direct access to database is deprecated, please use one of the available methods such as, SupabaseClient.from(_:), SupabaseClient.rpc(_:params:), or SupabaseClient.schema(_:)."
16 | )
17 | public var database: PostgrestClient {
18 | rest
19 | }
20 |
21 | /// Realtime client for Supabase
22 | @available(*, deprecated, message: "Use realtimeV2")
23 | public var realtime: RealtimeClient {
24 | _realtime.value
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Examples/Examples/UIViewControllerWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewControllerWrapper.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 10/04/24.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import SwiftUI
10 |
11 | struct UIViewControllerWrapper: UIViewControllerRepresentable {
12 | typealias UIViewControllerType = T
13 |
14 | let viewController: T
15 |
16 | init(_ viewController: T) {
17 | self.viewController = viewController
18 | }
19 |
20 | func makeUIViewController(context _: Context) -> T {
21 | viewController
22 | }
23 |
24 | func updateUIViewController(_: T, context _: Context) {
25 | // Update the view controller if needed
26 | }
27 | }
28 | #endif
29 |
--------------------------------------------------------------------------------
/Tests/StorageTests/BucketOptionsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Storage
4 |
5 | final class BucketOptionsTests: XCTestCase {
6 | func testDefaultInitialization() {
7 | let options = BucketOptions()
8 |
9 | XCTAssertFalse(options.public)
10 | XCTAssertNil(options.fileSizeLimit)
11 | XCTAssertNil(options.allowedMimeTypes)
12 | }
13 |
14 | func testCustomInitialization() {
15 | let options = BucketOptions(
16 | public: true,
17 | fileSizeLimit: "5242880",
18 | allowedMimeTypes: ["image/jpeg", "image/png"]
19 | )
20 |
21 | XCTAssertTrue(options.public)
22 | XCTAssertEqual(options.fileSizeLimit, "5242880")
23 | XCTAssertEqual(options.allowedMimeTypes, ["image/jpeg", "image/png"])
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Helpers/SharedModels/PostgrestError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgrestError.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 27/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct PostgrestError: Error, Codable, Sendable {
11 | public let detail: String?
12 | public let hint: String?
13 | public let code: String?
14 | public let message: String
15 |
16 | public init(
17 | detail: String? = nil,
18 | hint: String? = nil,
19 | code: String? = nil,
20 | message: String
21 | ) {
22 | self.hint = hint
23 | self.detail = detail
24 | self.code = code
25 | self.message = message
26 | }
27 | }
28 |
29 | extension PostgrestError: LocalizedError {
30 | public var errorDescription: String? {
31 | message
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Auth/AuthStateChangeListener.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthStateChangeListener.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 17/02/24.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 |
11 |
12 | /// A listener that can be removed by calling ``AuthStateChangeListenerRegistration/remove()``.
13 | ///
14 | /// - Note: Listener is automatically removed on deinit.
15 | public protocol AuthStateChangeListenerRegistration: Sendable {
16 | /// Removes the listener. After the initial call, subsequent calls have no effect.
17 | func remove()
18 | }
19 |
20 | extension ObservationToken: AuthStateChangeListenerRegistration {}
21 |
22 | public typealias AuthStateChangeListener = @Sendable (
23 | _ event: AuthChangeEvent,
24 | _ session: Session?
25 | ) -> Void
26 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/JWTAlgorithm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JWTVerifier.swift
3 | // Supabase
4 | //
5 | // Created by Claude on 06/10/25.
6 | //
7 |
8 | import Foundation
9 |
10 | enum JWTAlgorithm: String {
11 | case rs256 = "RS256"
12 |
13 | func verify(
14 | jwt: DecodedJWT,
15 | jwk: JWK
16 | ) -> Bool {
17 | let message = "\(jwt.raw.header).\(jwt.raw.payload)".data(using: .utf8)!
18 | switch self {
19 | case .rs256:
20 | #if canImport(Security)
21 | return SecKeyVerifySignature(
22 | jwk.rsaPublishKey!,
23 | .rsaSignatureMessagePKCS1v15SHA256,
24 | message as CFData,
25 | jwt.signature as CFData,
26 | nil
27 | )
28 | #else
29 | return false
30 | #endif
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Storage/Codable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Codable.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 18/10/23.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 |
11 | extension JSONEncoder {
12 | @available(*, deprecated, message: "Access to storage encoder is going to be removed.")
13 | public static let defaultStorageEncoder: JSONEncoder = {
14 | let encoder = JSONEncoder()
15 | encoder.keyEncodingStrategy = .convertToSnakeCase
16 | return encoder
17 | }()
18 |
19 | static let unconfiguredEncoder: JSONEncoder = .init()
20 | }
21 |
22 | extension JSONDecoder {
23 | @available(*, deprecated, message: "Access to storage decoder is going to be removed.")
24 | public static let defaultStorageDecoder: JSONDecoder = {
25 | JSONDecoder.supabase()
26 | }()
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/StorageTests/SupabaseStorageClient+Test.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SupabaseStorageClient+Test.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 04/11/23.
6 | //
7 |
8 | import Foundation
9 | import Storage
10 |
11 | extension SupabaseStorageClient {
12 | static func test(
13 | supabaseURL: String,
14 | apiKey: String,
15 | session: StorageHTTPSession = .init()
16 | ) -> SupabaseStorageClient {
17 | SupabaseStorageClient(
18 | configuration: StorageClientConfiguration(
19 | url: URL(string: supabaseURL)!,
20 | headers: [
21 | "Authorization": "Bearer \(apiKey)",
22 | "Apikey": apiKey,
23 | "X-Client-Info": "storage-swift/x.y.z",
24 | ],
25 | session: session,
26 | logger: nil
27 | )
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/PostgREST/Defaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Defaults.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 14/12/23.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 |
11 | let version = Helpers.version
12 |
13 | extension PostgrestClient.Configuration {
14 | /// The default `JSONDecoder` instance for ``PostgrestClient`` responses.
15 | public static let jsonDecoder: JSONDecoder = {
16 | JSONDecoder.supabase()
17 | }()
18 |
19 | /// The default `JSONEncoder` instance for ``PostgrestClient`` requests.
20 | public static let jsonEncoder: JSONEncoder = {
21 | JSONEncoder.supabase()
22 | }()
23 |
24 | /// The default headers for ``PostgrestClient`` requests.
25 | public static let defaultHeaders: [String: String] = [
26 | "X-Client-Info": "postgrest-swift/\(version)"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/URLOpener.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLOpener.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 17/05/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(WatchKit)
11 | import WatchKit
12 | #endif
13 |
14 | #if canImport(UIKit)
15 | import UIKit
16 | #endif
17 |
18 | #if canImport(AppKit)
19 | import AppKit
20 | #endif
21 |
22 | struct URLOpener {
23 | var open: @MainActor @Sendable (_ url: URL) -> Void
24 | }
25 |
26 | extension URLOpener {
27 | static var live: Self {
28 | URLOpener { url in
29 | #if os(macOS)
30 | NSWorkspace.shared.open(url)
31 | #elseif os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst)
32 | UIApplication.shared.open(url)
33 | #elseif os(watchOS)
34 | WKExtension.shared().openSystemURL(url)
35 | #endif
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/conventional-commits.yml:
--------------------------------------------------------------------------------
1 | # See https://github.com/amannn/action-semantic-pull-request
2 | name: 'PR Title is Conventional'
3 |
4 | on:
5 | pull_request:
6 | types:
7 | - opened
8 | - edited
9 | - synchronize
10 |
11 | permissions:
12 | pull-requests: write
13 | contents: read
14 |
15 | jobs:
16 | main:
17 | name: Validate PR title
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: amannn/action-semantic-pull-request@v6
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | with:
24 | subjectPattern: ^(?![A-Z]).+$
25 | subjectPatternError: |
26 | The subject "{subject}" found in the pull request title "{title}"
27 | didn't match the configured pattern. Please ensure that the subject
28 | doesn't start with an uppercase character.
29 |
--------------------------------------------------------------------------------
/Examples/Examples/TodoListRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoListRow.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 23/12/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct TodoListRow: View {
11 | let todo: Todo
12 | let completeTapped: () -> Void
13 |
14 | var body: some View {
15 | HStack {
16 | Text(todo.description)
17 | Spacer()
18 | Button {
19 | completeTapped()
20 | } label: {
21 | Image(systemName: todo.isComplete ? "checkmark.circle.fill" : "circle")
22 | }
23 | .buttonStyle(.plain)
24 | }
25 | }
26 | }
27 |
28 | struct TodoListRow_Previews: PreviewProvider {
29 | static var previews: some View {
30 | TodoListRow(
31 | todo: .init(
32 | id: UUID(),
33 | description: "",
34 | isComplete: false,
35 | createdAt: .now
36 | )
37 | ) {}
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-all-filters-and-count.txt:
--------------------------------------------------------------------------------
1 | curl \
2 | --header "Accept: application/json" \
3 | --header "Content-Type: application/json" \
4 | --header "X-Client-Info: postgrest-swift/x.y.z" \
5 | "https://example.supabase.co/todos?column=eq.Some%20value&column=neq.Some%20value&column=gt.Some%20value&column=gte.Some%20value&column=lt.Some%20value&column=lte.Some%20value&column=like.Some%20value&column=ilike.Some%20value&column=match.Some%20value&column=imatch.Some%20value&column=is.Some%20value&column=isdistinct.Some%20value&column=in.Some%20value&column=cs.Some%20value&column=cd.Some%20value&column=sl.Some%20value&column=sr.Some%20value&column=nxl.Some%20value&column=nxr.Some%20value&column=adj.Some%20value&column=ov.Some%20value&column=fts.Some%20value&column=plfts.Some%20value&column=phfts.Some%20value&column=wfts.Some%20value&select=*"
--------------------------------------------------------------------------------
/Sources/Auth/Storage/KeychainLocalStorage.swift:
--------------------------------------------------------------------------------
1 | #if !os(Windows) && !os(Linux) && !os(Android)
2 | import Foundation
3 |
4 | /// ``AuthLocalStorage`` implementation using Keychain. This is the default local storage used by the library.
5 | public struct KeychainLocalStorage: AuthLocalStorage {
6 | private let keychain: Keychain
7 |
8 | public init(service: String? = "supabase.gotrue.swift", accessGroup: String? = nil) {
9 | keychain = Keychain(service: service, accessGroup: accessGroup)
10 | }
11 |
12 | public func store(key: String, value: Data) throws {
13 | try keychain.set(value, forKey: key)
14 | }
15 |
16 | public func retrieve(key: String) throws -> Data? {
17 | try keychain.data(forKey: key)
18 | }
19 |
20 | public func remove(key: String) throws {
21 | try keychain.deleteItem(forKey: key)
22 | }
23 | }
24 | #endif
25 |
--------------------------------------------------------------------------------
/Tests/StorageTests/FileOptionsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Storage
4 |
5 | final class FileOptionsTests: XCTestCase {
6 | func testDefaultInitialization() {
7 | let options = FileOptions()
8 |
9 | XCTAssertEqual(options.cacheControl, "3600")
10 | XCTAssertNil(options.contentType)
11 | XCTAssertFalse(options.upsert)
12 | XCTAssertNil(options.metadata)
13 | }
14 |
15 | func testCustomInitialization() {
16 | let metadata: [String: AnyJSON] = ["key": .string("value")]
17 | let options = FileOptions(
18 | cacheControl: "7200",
19 | contentType: "image/jpeg",
20 | upsert: true,
21 | metadata: metadata
22 | )
23 |
24 | XCTAssertEqual(options.cacheControl, "7200")
25 | XCTAssertEqual(options.contentType, "image/jpeg")
26 | XCTAssertTrue(options.upsert)
27 | XCTAssertEqual(options.metadata?["key"], .string("value"))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Examples/SlackClone/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 23/01/24.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 | import Supabase
11 |
12 | extension Logger {
13 | static let main = Self(subsystem: "com.supabase.slack-clone", category: "app")
14 | static let supabase = Self(subsystem: "com.supabase.slack-clone", category: "supabase")
15 | }
16 |
17 | struct SupaLogger: SupabaseLogger {
18 | func log(message: SupabaseLogMessage) {
19 | Task { @MainActor in
20 | let logger = Logger.supabase
21 |
22 | switch message.level {
23 | case .debug: logger.debug("\(message, privacy: .public)")
24 | case .error: logger.error("\(message, privacy: .public)")
25 | case .verbose: logger.info("\(message, privacy: .public)")
26 | case .warning: logger.notice("\(message, privacy: .public)")
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Storage/StorageHTTPClient.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if canImport(FoundationNetworking)
4 | import FoundationNetworking
5 | #endif
6 |
7 | public struct StorageHTTPSession: Sendable {
8 | public var fetch: @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse)
9 | public var upload:
10 | @Sendable (_ request: URLRequest, _ data: Data) async throws -> (Data, URLResponse)
11 |
12 | public init(
13 | fetch: @escaping @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse),
14 | upload: @escaping @Sendable (_ request: URLRequest, _ data: Data) async throws -> (
15 | Data, URLResponse
16 | )
17 | ) {
18 | self.fetch = fetch
19 | self.upload = upload
20 | }
21 |
22 | public init(session: URLSession = .shared) {
23 | self.init(
24 | fetch: { try await session.data(for: $0) },
25 | upload: { try await session.upload(for: $0, from: $1) }
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/HelpersTests/WithTimeoutTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WithTimeoutTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 19/04/24.
6 | //
7 |
8 | import Foundation
9 | import Helpers
10 | import XCTest
11 |
12 | final class WithTimeoutTests: XCTestCase {
13 | // func testWithTimeout() async {
14 | // do {
15 | // try await withTimeout(interval: 0.25) {
16 | // try await Task.sleep(nanoseconds: NSEC_PER_SEC)
17 | // }
18 | // XCTFail("Task should timeout.")
19 | // } catch {
20 | // XCTAssertTrue(error is TimeoutError)
21 | // }
22 | //
23 | // do {
24 | // let answer = try await withTimeout(interval: 1.25) {
25 | // try await Task.sleep(nanoseconds: NSEC_PER_SEC)
26 | // return 42
27 | // }
28 | //
29 | // XCTAssertEqual(answer, 42)
30 | // } catch {
31 | // XCTFail("Should not throw error: \(error)")
32 | // }
33 | // }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/RealtimeTests/ExportsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportsTests.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 29/07/25.
6 | //
7 |
8 | import XCTest
9 |
10 | @testable import Realtime
11 |
12 | final class ExportsTests: XCTestCase {
13 | func testHelperImportIsAccessible() {
14 | // Test that the Helpers module is properly exported
15 | // This is a simple validation that the @_exported import works
16 |
17 | // Test that we can access JSONObject from Helpers via Realtime
18 | let jsonObject: JSONObject = [:]
19 | XCTAssertNotNil(jsonObject)
20 |
21 | // Test that we can access AnyJSON from Helpers via Realtime
22 | let anyJSON: AnyJSON = .string("test")
23 | XCTAssertEqual(anyJSON, .string("test"))
24 |
25 | // Test that we can access ObservationToken from Helpers via Realtime
26 | let token = ObservationToken {
27 | // Empty cleanup
28 | }
29 | XCTAssertNotNil(token)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Helpers/HTTP/HTTPResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPResponse.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 30/04/24.
6 | //
7 |
8 | import Foundation
9 | import HTTPTypes
10 |
11 | #if canImport(FoundationNetworking)
12 | import FoundationNetworking
13 | #endif
14 |
15 | package struct HTTPResponse: Sendable {
16 | package let data: Data
17 | package let headers: HTTPFields
18 | package let statusCode: Int
19 |
20 | package let underlyingResponse: HTTPURLResponse
21 |
22 | package init(data: Data, response: HTTPURLResponse) {
23 | self.data = data
24 | headers = HTTPFields(response.allHeaderFields as? [String: String] ?? [:])
25 | statusCode = response.statusCode
26 | underlyingResponse = response
27 | }
28 | }
29 |
30 | extension HTTPResponse {
31 | package func decoded(as _: T.Type = T.self, decoder: JSONDecoder = JSONDecoder()) throws -> T {
32 | try decoder.decode(T.self, from: data)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/AuthTests/Mocks/Mocks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mocks.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 27/10/23.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 | import TestHelpers
11 | import XCTestDynamicOverlay
12 |
13 | @testable import Auth
14 |
15 | let clientURL = URL(string: "http://localhost:54321/auth/v1")!
16 |
17 | extension Session {
18 | static let validSession = Session(
19 | accessToken: "accesstoken",
20 | tokenType: "bearer",
21 | expiresIn: 120,
22 | expiresAt: Date().addingTimeInterval(120).timeIntervalSince1970,
23 | refreshToken: "refreshtoken",
24 | user: User(fromMockNamed: "user")
25 | )
26 |
27 | static let expiredSession = Session(
28 | accessToken: "accesstoken",
29 | tokenType: "bearer",
30 | expiresIn: 30,
31 | expiresAt: Date().addingTimeInterval(30).timeIntervalSince1970,
32 | refreshToken: "refreshtoken",
33 | user: User(fromMockNamed: "user")
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/Examples/Examples/Models.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Todo: Identifiable, Hashable, Decodable {
4 | let id: UUID
5 | var description: String
6 | var isComplete: Bool
7 | let createdAt: Date
8 |
9 | enum CodingKeys: String, CodingKey {
10 | case id
11 | case description
12 | case isComplete = "is_complete"
13 | case createdAt = "created_at"
14 | }
15 | }
16 |
17 | struct CreateTodoRequest: Encodable {
18 | var description: String
19 | var isComplete: Bool
20 | var ownerID: UUID
21 |
22 | enum CodingKeys: String, CodingKey {
23 | case description
24 | case isComplete = "is_complete"
25 | case ownerID = "owner_id"
26 | }
27 | }
28 |
29 | struct UpdateTodoRequest: Encodable {
30 | var description: String?
31 | var isComplete: Bool?
32 | var ownerID: UUID
33 |
34 | enum CodingKeys: String, CodingKey {
35 | case description
36 | case isComplete = "is_complete"
37 | case ownerID = "owner_id"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Helpers/SharedModels/HTTPError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPError.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 07/05/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(FoundationNetworking)
11 | import FoundationNetworking
12 | #endif
13 |
14 | /// A generic error from a HTTP request.
15 | ///
16 | /// Contains both the `Data` and `HTTPURLResponse` which you can use to extract more information about it.
17 | public struct HTTPError: Error, Sendable {
18 | public let data: Data
19 | public let response: HTTPURLResponse
20 |
21 | public init(data: Data, response: HTTPURLResponse) {
22 | self.data = data
23 | self.response = response
24 | }
25 | }
26 |
27 | extension HTTPError: LocalizedError {
28 | public var errorDescription: String? {
29 | var message = "Status Code: \(response.statusCode)"
30 | if let body = String(data: data, encoding: .utf8) {
31 | message += " Body: \(body)"
32 | }
33 | return message
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Helpers/Task+withTimeout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Task+withTimeout.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 19/04/24.
6 | //
7 |
8 | import Foundation
9 |
10 | @discardableResult
11 | package func withTimeout(
12 | interval: TimeInterval,
13 | @_inheritActorContext operation: @escaping @Sendable () async -> R
14 | ) async throws -> R {
15 | try await withThrowingTaskGroup(of: R.self) { group in
16 | defer {
17 | group.cancelAll()
18 | }
19 |
20 | let deadline = Date(timeIntervalSinceNow: interval)
21 |
22 | group.addTask {
23 | await operation()
24 | }
25 |
26 | group.addTask {
27 | let interval = deadline.timeIntervalSinceNow
28 | if interval > 0 {
29 | try await _clock.sleep(for: interval)
30 | }
31 | try Task.checkCancellation()
32 | throw TimeoutError()
33 | }
34 |
35 | return try await group.next()!
36 | }
37 | }
38 |
39 | package struct TimeoutError: Error, Hashable {}
40 |
--------------------------------------------------------------------------------
/Tests/HelpersTests/ObservationTokenTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservationTokenTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 17/02/24.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 | import Helpers
11 | import XCTest
12 |
13 | final class ObservationTokenTests: XCTestCase {
14 | func testRemove() {
15 | let handle = ObservationToken()
16 |
17 | let onRemoveCallCount = LockIsolated(0)
18 | handle.onCancel = {
19 | onRemoveCallCount.withValue {
20 | $0 += 1
21 | }
22 | }
23 |
24 | handle.cancel()
25 | handle.cancel()
26 |
27 | XCTAssertEqual(onRemoveCallCount.value, 1)
28 | }
29 |
30 | func testDeinit() {
31 | var handle: ObservationToken? = ObservationToken()
32 |
33 | let onRemoveCallCount = LockIsolated(0)
34 | handle?.onCancel = {
35 | onRemoveCallCount.withValue {
36 | $0 += 1
37 | }
38 | }
39 |
40 | handle = nil
41 |
42 | XCTAssertEqual(onRemoveCallCount.value, 1)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/EventEmitter.swift:
--------------------------------------------------------------------------------
1 | import ConcurrencyExtras
2 | import Foundation
3 |
4 | struct AuthStateChangeEventEmitter {
5 | var emitter = EventEmitter<(AuthChangeEvent, Session?)?>(initialEvent: nil, emitsLastEventWhenAttaching: false)
6 | var logger: (any SupabaseLogger)?
7 |
8 | func attach(_ listener: @escaping AuthStateChangeListener) -> ObservationToken {
9 | emitter.attach { event in
10 | guard let event else { return }
11 | listener(event.0, event.1)
12 |
13 | logger?.verbose("Auth state changed: \(event)")
14 | }
15 | }
16 |
17 | func emit(_ event: AuthChangeEvent, session: Session?, token: ObservationToken? = nil) {
18 | NotificationCenter.default.post(
19 | name: AuthClient.didChangeAuthStateNotification,
20 | object: nil,
21 | userInfo: [
22 | AuthClient.authChangeEventInfoKey: event,
23 | AuthClient.authChangeSessionInfoKey: session as Any,
24 | ]
25 | )
26 |
27 | emitter.emit((event, session), to: token)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/FixedWidthInteger+Random.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // Borrowed from the Vapor project,
4 | // https://github.com/vapor/vapor/blob/main/Sources/Vapor/Utilities/Array%2BRandom.swift#L14
5 | extension FixedWidthInteger {
6 | static func random() -> Self {
7 | random(in: .min ... .max)
8 | }
9 |
10 | static func random(using generator: inout some RandomNumberGenerator) -> Self {
11 | random(in: .min ... .max, using: &generator)
12 | }
13 | }
14 |
15 | extension Array where Element: FixedWidthInteger {
16 | static func random(count: Int) -> [Element] {
17 | var array: [Element] = .init(repeating: 0, count: count)
18 | (0 ..< count).forEach { array[$0] = Element.random() }
19 | return array
20 | }
21 |
22 | static func random(count: Int, using generator: inout some RandomNumberGenerator) -> [Element] {
23 | var array: [Element] = .init(repeating: 0, count: count)
24 | (0 ..< count).forEach { array[$0] = Element.random(using: &generator) }
25 | return array
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Examples/supabase/functions/hello-world/index.ts:
--------------------------------------------------------------------------------
1 | // Follow this setup guide to integrate the Deno language server with your editor:
2 | // https://deno.land/manual/getting_started/setup_your_environment
3 | // This enables autocomplete, go to definition, etc.
4 |
5 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
6 |
7 | console.log("Hello from Functions!")
8 |
9 | serve(async (req) => {
10 | const { name } = await req.json()
11 | const data = {
12 | message: `Hello ${name}!`,
13 | }
14 |
15 | return new Response(
16 | JSON.stringify(data),
17 | { headers: { "Content-Type": "application/json" } },
18 | )
19 | })
20 |
21 | // To invoke:
22 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
23 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
24 | // --header 'Content-Type: application/json' \
25 | // --data '{"name":"Functions"}'
26 |
--------------------------------------------------------------------------------
/Tests/AuthTests/Resources/signup-response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "859f402d-b3de-4105-a1b9-932836d9193b",
3 | "aud": "authenticated",
4 | "role": "authenticated",
5 | "email": "guilherme@grds.dev",
6 | "phone": "",
7 | "confirmation_sent_at": "2022-04-09T11:57:01.710600634Z",
8 | "app_metadata": {
9 | "provider": "email",
10 | "providers": [
11 | "email"
12 | ]
13 | },
14 | "user_metadata": {},
15 | "identities": [
16 | {
17 | "id": "859f402d-b3de-4105-a1b9-932836d9193b",
18 | "user_id": "859f402d-b3de-4105-a1b9-932836d9193b",
19 | "identity_id": "859f402d-b3de-4105-a1b9-932836d9193b",
20 | "identity_data": {
21 | "sub": "859f402d-b3de-4105-a1b9-932836d9193b"
22 | },
23 | "provider": "email",
24 | "last_sign_in_at": "2022-04-09T11:23:45.899902Z",
25 | "created_at": "2022-04-09T11:23:45.899924Z",
26 | "updated_at": "2022-04-09T11:23:45.899926Z"
27 | }
28 | ],
29 | "created_at": "2022-04-09T11:23:45.874827Z",
30 | "updated_at": "2022-04-09T11:57:01.720803Z"
31 | }
32 |
--------------------------------------------------------------------------------
/Examples/UserManagement/Supabase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Supabase.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 | import Supabase
11 |
12 | let supabase = SupabaseClient(
13 | supabaseURL: URL(string: "http://127.0.0.1:54321")!,
14 | supabaseKey:
15 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU",
16 | options: .init(
17 | global: .init(logger: AppLogger())
18 | )
19 | )
20 |
21 | struct AppLogger: SupabaseLogger {
22 | let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "supabase")
23 |
24 | func log(message: SupabaseLogMessage) {
25 | switch message.level {
26 | case .verbose:
27 | logger.log(level: .info, "\(message.description)")
28 | case .debug:
29 | logger.log(level: .debug, "\(message.description)")
30 | case .warning, .error:
31 | logger.log(level: .error, "\(message.description)")
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Examples/Examples/Auth/AuthController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthController.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 22/12/22.
6 | //
7 |
8 | import Auth
9 | import SwiftUI
10 |
11 | @Observable
12 | @MainActor
13 | final class AuthController {
14 | var session: Session?
15 | var isPasswordRecoveryFlow: Bool = false
16 |
17 | var currentUserID: UUID {
18 | guard let id = session?.user.id else {
19 | preconditionFailure("Required session.")
20 | }
21 |
22 | return id
23 | }
24 |
25 | @ObservationIgnored
26 | private var observeAuthStateChangesTask: Task?
27 |
28 | init() {
29 | observeAuthStateChangesTask = Task {
30 | for await (event, session) in supabase.auth.authStateChanges {
31 | if [.initialSession, .signedIn, .signedOut].contains(event) {
32 | self.session = session
33 | }
34 |
35 | if event == .passwordRecovery {
36 | self.isPasswordRecoveryFlow = true
37 | }
38 | }
39 | }
40 | }
41 |
42 | deinit {
43 | observeAuthStateChangesTask?.cancel()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Examples/SlackClone/Supabase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Supabase.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 27/12/23.
6 | //
7 |
8 | import Foundation
9 | import Supabase
10 |
11 | let encoder: JSONEncoder = {
12 | let encoder = PostgrestClient.Configuration.jsonEncoder
13 | encoder.keyEncodingStrategy = .convertToSnakeCase
14 | return encoder
15 | }()
16 |
17 | let decoder: JSONDecoder = {
18 | let decoder = PostgrestClient.Configuration.jsonDecoder
19 | decoder.keyDecodingStrategy = .convertFromSnakeCase
20 | return decoder
21 | }()
22 |
23 | let supabase = SupabaseClient(
24 | supabaseURL: URL(string: "http://127.0.0.1:54321")!,
25 | supabaseKey:
26 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0",
27 | options: SupabaseClientOptions(
28 | db: .init(encoder: encoder, decoder: decoder),
29 | auth: .init(redirectToURL: URL(string: "com.supabase.slack-clone://login-callback")),
30 | global: SupabaseClientOptions.GlobalOptions(logger: SupaLogger())
31 | )
32 | )
33 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/Helpers.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Extracts parameters encoded in the URL both in the query and fragment.
4 | func extractParams(from url: URL) -> [String: String] {
5 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
6 | return [:]
7 | }
8 |
9 | var result: [String: String] = [:]
10 |
11 | if let fragment = components.fragment {
12 | let items = extractParams(from: fragment)
13 | for item in items {
14 | result[item.name] = item.value
15 | }
16 | }
17 |
18 | if let items = components.queryItems {
19 | for item in items {
20 | result[item.name] = item.value
21 | }
22 | }
23 |
24 | return result
25 | }
26 |
27 | private func extractParams(from fragment: String) -> [URLQueryItem] {
28 | let components =
29 | fragment
30 | .split(separator: "&")
31 | .map { $0.split(separator: "=") }
32 |
33 | return
34 | components
35 | .compactMap {
36 | $0.count == 2
37 | ? URLQueryItem(name: String($0[0]), value: String($0[1]))
38 | : nil
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Supabase
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/Dependencies.swift:
--------------------------------------------------------------------------------
1 | import ConcurrencyExtras
2 | import Foundation
3 |
4 | struct Dependencies: Sendable {
5 | var configuration: AuthClient.Configuration
6 | var http: any HTTPClientType
7 | var api: APIClient
8 | var codeVerifierStorage: CodeVerifierStorage
9 | var sessionStorage: SessionStorage
10 | var sessionManager: SessionManager
11 |
12 | var eventEmitter = AuthStateChangeEventEmitter()
13 | var date: @Sendable () -> Date = { Date() }
14 |
15 | var urlOpener: URLOpener = .live
16 | var pkce: PKCE = .live
17 | var logger: (any SupabaseLogger)?
18 |
19 | var encoder: JSONEncoder { configuration.encoder }
20 | var decoder: JSONDecoder { configuration.decoder }
21 | }
22 |
23 | extension Dependencies {
24 | static let instances = LockIsolated([AuthClientID: Dependencies]())
25 |
26 | static subscript(_ id: AuthClientID) -> Dependencies {
27 | get {
28 | guard let instance = instances[id] else {
29 | fatalError("Dependencies not found for id: \(id)")
30 | }
31 | return instance
32 | }
33 | set {
34 | instances.withValue { $0[id] = newValue }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Helpers/Base64URL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Base64URL.swift
3 | // Supabase
4 | //
5 | // Created by Claude on 06/10/25.
6 | //
7 |
8 | import Foundation
9 |
10 | package enum Base64URL {
11 | /// Decodes a base64url-encoded string to Data
12 | package static func decode(_ value: String) -> Data? {
13 | var base64 = value.replacingOccurrences(of: "-", with: "+")
14 | .replacingOccurrences(of: "_", with: "/")
15 | let length = Double(base64.lengthOfBytes(using: .utf8))
16 | let requiredLength = 4 * ceil(length / 4.0)
17 | let paddingLength = requiredLength - length
18 | if paddingLength > 0 {
19 | let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
20 | base64 = base64 + padding
21 | }
22 | return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
23 | }
24 |
25 | /// Encodes Data to a base64url-encoded string
26 | package static func encode(_ data: Data) -> String {
27 | data.base64EncodedString()
28 | .replacingOccurrences(of: "+", with: "-")
29 | .replacingOccurrences(of: "/", with: "_")
30 | .replacingOccurrences(of: "=", with: "")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Examples/SlackClone/Dependencies.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dependencies.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 04/01/24.
6 | //
7 |
8 | import Foundation
9 | import Supabase
10 |
11 | @MainActor
12 | class Dependencies {
13 | static let shared = Dependencies()
14 |
15 | let channel = ChannelStore.shared
16 | let users = UserStore.shared
17 | let messages = MessageStore.shared
18 | }
19 |
20 | struct User: Codable, Identifiable, Hashable {
21 | var id: UUID
22 | var username: String
23 | }
24 |
25 | struct AddChannel: Encodable {
26 | var slug: String
27 | var createdBy: UUID
28 | }
29 |
30 | struct Channel: Identifiable, Codable, Hashable {
31 | var id: Int
32 | var slug: String
33 | var insertedAt: Date
34 | }
35 |
36 | struct Message: Identifiable, Codable, Hashable {
37 | var id: Int
38 | var insertedAt: Date
39 | var message: String
40 | var user: User
41 | var channel: Channel
42 | }
43 |
44 | struct NewMessage: Codable {
45 | var message: String
46 | var userId: UUID
47 | let channelId: Int
48 | }
49 |
50 | struct UserPresence: Codable, Hashable {
51 | var userId: UUID
52 | var onlineAt: Date
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Storage/BucketOptions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct BucketOptions: Sendable {
4 | /// The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. Bu default, buckets are private.
5 | public var `public`: Bool
6 | /// Specifies the allowed mime types that this bucket can accept during upload. The default value is null, which allows files with all mime types to be uploaded. Each mime type specified can be a wildcard, e.g. image/*, or a specific mime type, e.g. image/png.
7 | public var fileSizeLimit: String?
8 | /// Specifies the max file size in bytes that can be uploaded to this bucket. The global file size limit takes precedence over this value. The default value is null, which doesn't set a per bucket file size limit.
9 | public var allowedMimeTypes: [String]?
10 |
11 | public init(
12 | public: Bool = false,
13 | fileSizeLimit: String? = nil,
14 | allowedMimeTypes: [String]? = nil
15 | ) {
16 | self.public = `public`
17 | self.fileSizeLimit = fileSizeLimit
18 | self.allowedMimeTypes = allowedMimeTypes
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Realtime/RealtimePostgresFilterValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimePostgresFilterValue.swift
3 | // Supabase
4 | //
5 | // Created by Lucas Abijmil on 19/02/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A value that can be used to filter Realtime changes in a channel.
11 | public protocol RealtimePostgresFilterValue {
12 | var rawValue: String { get }
13 | }
14 |
15 | extension String: RealtimePostgresFilterValue {
16 | public var rawValue: String { self }
17 | }
18 |
19 | extension Int: RealtimePostgresFilterValue {
20 | public var rawValue: String { "\(self)" }
21 | }
22 |
23 | extension Double: RealtimePostgresFilterValue {
24 | public var rawValue: String { "\(self)" }
25 | }
26 |
27 | extension Bool: RealtimePostgresFilterValue {
28 | public var rawValue: String { "\(self)" }
29 | }
30 |
31 | extension UUID: RealtimePostgresFilterValue {
32 | public var rawValue: String { uuidString }
33 | }
34 |
35 | extension Date: RealtimePostgresFilterValue {
36 | public var rawValue: String {
37 | let formatter = ISO8601DateFormatter()
38 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
39 | return formatter.string(from: self)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Examples/supabase/migrations/20221223094509_init.sql:
--------------------------------------------------------------------------------
1 | create table todos(
2 | id uuid default uuid_generate_v4() primary key not null,
3 | description text not null,
4 | is_complete boolean not null,
5 | created_at timestamptz default (now() at time zone 'utc'::text) not null,
6 | owner_id uuid references auth.users(id) not null
7 | );
8 |
9 | alter table todos enable row level security;
10 |
11 | create policy "Allow access to owner only" on todos as permissive
12 | for all to authenticated
13 | using (auth.uid() = owner_id)
14 | with check (auth.uid() = owner_id);
15 |
16 | -- Storage
17 | create policy "Allow authenticated users to create buckets." on storage.buckets
18 | for insert to authenticated
19 | with check (true);
20 |
21 | create policy "Allow authenticated users to list buckets." on storage.buckets
22 | for select to authenticated
23 | using (true);
24 |
25 | create policy "Allow authenticated users to upload objects." on storage.objects
26 | for insert to authenticated
27 | with check (true);
28 |
29 | create policy "Allow authenticated users to list objects." on storage.objects
30 | for select to authenticated
31 | using (true);
32 |
33 |
--------------------------------------------------------------------------------
/Sources/Auth/Defaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Defaults.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 14/12/23.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 |
11 | extension AuthClient.Configuration {
12 | /// The default JSONEncoder instance used by the ``AuthClient``.
13 | public static let jsonEncoder: JSONEncoder = {
14 | let encoder = JSONEncoder.supabase()
15 | encoder.keyEncodingStrategy = .convertToSnakeCase
16 | return encoder
17 | }()
18 |
19 | /// The default JSONDecoder instance used by the ``AuthClient``.
20 | public static let jsonDecoder: JSONDecoder = {
21 | let decoder = JSONDecoder.supabase()
22 | decoder.keyDecodingStrategy = .convertFromSnakeCase
23 | return decoder
24 | }()
25 |
26 | /// The default headers used by the ``AuthClient``.
27 | public static let defaultHeaders: [String: String] = [
28 | "X-Client-Info": "auth-swift/\(version)"
29 | ]
30 |
31 | /// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance.
32 | public static let defaultFlowType: AuthFlowType = .pkce
33 |
34 | /// The default value when initializing a ``AuthClient`` instance.
35 | public static let defaultAutoRefreshToken: Bool = true
36 | }
37 |
--------------------------------------------------------------------------------
/Examples/Examples/AddTodoListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddTodoListView.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 23/12/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AddTodoListView: View {
11 | @Binding var request: CreateTodoRequest
12 | let completion: (Result) -> Void
13 |
14 | var body: some View {
15 | Section {
16 | TextField("Description", text: $request.description)
17 | Button("Save") {
18 | Task { await saveButtonTapped() }
19 | }
20 | }
21 | }
22 |
23 | func saveButtonTapped() async {
24 | do {
25 | let createdTodo: Todo = try await supabase.from("todos")
26 | .insert(request, returning: .representation)
27 | .single()
28 | .execute()
29 | .value
30 | completion(.success(createdTodo))
31 | } catch {
32 | completion(.failure(error))
33 | }
34 | }
35 | }
36 |
37 | struct AddTodoListView_Previews: PreviewProvider {
38 | static var previews: some View {
39 | AddTodoListView(
40 | request: .constant(
41 | .init(
42 | description: "",
43 | isComplete: false,
44 | ownerID: UUID()
45 | )
46 | )
47 | ) { _ in
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/StorageTests/StorageErrorTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Storage
4 |
5 | final class StorageErrorTests: XCTestCase {
6 | func testErrorInitialization() {
7 | let error = StorageError(
8 | statusCode: "404",
9 | message: "File not found",
10 | error: "NotFound"
11 | )
12 |
13 | XCTAssertEqual(error.statusCode, "404")
14 | XCTAssertEqual(error.message, "File not found")
15 | XCTAssertEqual(error.error, "NotFound")
16 | }
17 |
18 | func testLocalizedError() {
19 | let error = StorageError(
20 | statusCode: "500",
21 | message: "Internal server error",
22 | error: nil
23 | )
24 |
25 | XCTAssertEqual(error.errorDescription, "Internal server error")
26 | }
27 |
28 | func testDecoding() throws {
29 | let json = """
30 | {
31 | "statusCode": "403",
32 | "message": "Unauthorized access",
33 | "error": "Forbidden"
34 | }
35 | """.data(using: .utf8)!
36 |
37 | let error = try JSONDecoder().decode(StorageError.self, from: json)
38 |
39 | XCTAssertEqual(error.statusCode, "403")
40 | XCTAssertEqual(error.message, "Unauthorized access")
41 | XCTAssertEqual(error.error, "Forbidden")
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 22/05/24.
6 | //
7 |
8 | import Foundation
9 | import HTTPTypes
10 |
11 | let defaultAuthURL = URL(string: "http://localhost:9999")!
12 | let defaultExpiryMargin: TimeInterval = 30
13 |
14 | let autoRefreshTickDuration: TimeInterval = 30
15 | let autoRefreshTickThreshold = 3
16 |
17 | let defaultStorageKey = "supabase.auth.token"
18 |
19 | extension HTTPField.Name {
20 | static let apiVersionHeaderName = HTTPField.Name("X-Supabase-Api-Version")!
21 | }
22 |
23 | let apiVersions: [APIVersion.Name: APIVersion] = [
24 | ._20240101: ._20240101
25 | ]
26 |
27 | struct APIVersion {
28 | let timestamp: Date
29 | let name: Name
30 |
31 | enum Name: String {
32 | case _20240101 = "2024-01-01"
33 | }
34 |
35 | static func date(for name: Name) -> Date {
36 | let formatter = ISO8601DateFormatter()
37 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
38 | return formatter.date(from: "\(name.rawValue)T00:00:00.0Z")!
39 | }
40 | }
41 |
42 | extension APIVersion {
43 | static let _20240101 = APIVersion(
44 | timestamp: APIVersion.date(for: ._20240101),
45 | name: ._20240101
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
1 | -- Seed data for Examples App
2 | -- This file contains sample data for testing the examples
3 |
4 | -- Note: In production, you would create users through the auth system
5 | -- For local testing, we can insert some sample data once users are created through the app
6 |
7 | -- Sample function to create test data after a user signs up
8 | create or replace function create_sample_data_for_user(user_id uuid)
9 | returns void
10 | language plpgsql
11 | security definer
12 | as $$
13 | begin
14 | -- Create profile
15 | insert into profiles (id, username, full_name)
16 | values (user_id, 'demo_user', 'Demo User')
17 | on conflict (id) do nothing;
18 |
19 | -- Create sample todos
20 | insert into todos (description, is_complete, owner_id)
21 | values
22 | ('Welcome to Supabase Swift!', false, user_id),
23 | ('Try creating a new todo', false, user_id),
24 | ('Mark this todo as complete', false, user_id),
25 | ('Check out the Storage tab', false, user_id),
26 | ('Explore Realtime features', false, user_id);
27 |
28 | -- Create sample messages
29 | insert into messages (content, user_id, channel_id)
30 | values
31 | ('Welcome to the Examples app!', user_id, 'general'),
32 | ('This is a sample message', user_id, 'general');
33 | end;
34 | $$;
35 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/PKCE.swift:
--------------------------------------------------------------------------------
1 | import Crypto
2 | import Foundation
3 |
4 | struct PKCE {
5 | var generateCodeVerifier: @Sendable () -> String
6 | var generateCodeChallenge: @Sendable (_ codeVerifier: String) -> String
7 | }
8 |
9 | extension PKCE {
10 | static let live = PKCE(
11 | generateCodeVerifier: {
12 | let buffer = [UInt8].random(count: 64)
13 | return Data(buffer).pkceBase64EncodedString()
14 | },
15 | generateCodeChallenge: { codeVerifier in
16 | guard let data = codeVerifier.data(using: .utf8) else {
17 | preconditionFailure("provided string should be utf8 encoded.")
18 | }
19 |
20 | var hasher = SHA256()
21 | hasher.update(data: data)
22 | let hashed = hasher.finalize()
23 | return Data(hashed).pkceBase64EncodedString()
24 | }
25 | )
26 | }
27 |
28 | extension Data {
29 | // Returns a base64 encoded string, replacing reserved characters
30 | // as per the PKCE spec https://tools.ietf.org/html/rfc7636#section-4.2
31 | func pkceBase64EncodedString() -> String {
32 | base64EncodedString()
33 | .replacingOccurrences(of: "+", with: "-")
34 | .replacingOccurrences(of: "/", with: "_")
35 | .replacingOccurrences(of: "=", with: "")
36 | .trimmingCharacters(in: .whitespaces)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/AuthTests/MockHelpers.swift:
--------------------------------------------------------------------------------
1 | import ConcurrencyExtras
2 | import Foundation
3 | import TestHelpers
4 |
5 | @testable import Auth
6 |
7 | func json(named name: String) -> Data {
8 | let url = Bundle.module.url(forResource: name, withExtension: "json")
9 | return try! Data(contentsOf: url!)
10 | }
11 |
12 | extension Decodable {
13 | init(fromMockNamed name: String) {
14 | self = try! AuthClient.Configuration.jsonDecoder.decode(Self.self, from: json(named: name))
15 | }
16 | }
17 |
18 | extension Dependencies {
19 | static var mock = Dependencies(
20 | configuration: AuthClient.Configuration(
21 | url: URL(string: "https://project-id.supabase.com")!,
22 | localStorage: InMemoryLocalStorage(),
23 | logger: nil
24 | ),
25 | http: HTTPClientMock(),
26 | api: APIClient(clientID: AuthClientID()),
27 | codeVerifierStorage: CodeVerifierStorage.mock,
28 | sessionStorage: SessionStorage.live(clientID: AuthClientID()),
29 | sessionManager: SessionManager.live(clientID: AuthClientID())
30 | )
31 | }
32 |
33 | extension CodeVerifierStorage {
34 | static var mock: CodeVerifierStorage {
35 | let code = LockIsolated(nil)
36 |
37 | return Self(
38 | get: { code.value },
39 | set: { code.setValue($0) }
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/Storage/SupabaseStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct StorageClientConfiguration: Sendable {
4 | public var url: URL
5 | public var headers: [String: String]
6 | public let encoder: JSONEncoder
7 | public let decoder: JSONDecoder
8 | public let session: StorageHTTPSession
9 | public let logger: (any SupabaseLogger)?
10 | public let useNewHostname: Bool
11 |
12 | public init(
13 | url: URL,
14 | headers: [String: String],
15 | encoder: JSONEncoder = .defaultStorageEncoder,
16 | decoder: JSONDecoder = .defaultStorageDecoder,
17 | session: StorageHTTPSession = .init(),
18 | logger: (any SupabaseLogger)? = nil,
19 | useNewHostname: Bool = false
20 | ) {
21 | self.url = url
22 | self.headers = headers
23 | self.encoder = encoder
24 | self.decoder = decoder
25 | self.session = session
26 | self.logger = logger
27 | self.useNewHostname = useNewHostname
28 | }
29 | }
30 |
31 | public class SupabaseStorageClient: StorageBucketApi, @unchecked Sendable {
32 | /// Perform file operation in a bucket.
33 | /// - Parameter id: The bucket id to operate on.
34 | /// - Returns: StorageFileApi object
35 | public func from(_ id: String) -> StorageFileApi {
36 | StorageFileApi(bucketId: id, configuration: configuration)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Examples/Examples/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Examples/SlackClone/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Examples/UserManagement/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/AuthTests/AuthClientMultipleInstancesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthClientMultipleInstancesTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 05/07/24.
6 | //
7 |
8 | import TestHelpers
9 | import XCTest
10 |
11 | @testable import Auth
12 |
13 | final class AuthClientMultipleInstancesTests: XCTestCase {
14 | func testMultipleAuthClientInstances() {
15 | let url = URL(string: "http://localhost:54321/auth")!
16 |
17 | let client1Storage = InMemoryLocalStorage()
18 | let client2Storage = InMemoryLocalStorage()
19 |
20 | let client1 = AuthClient(
21 | configuration: AuthClient.Configuration(
22 | url: url,
23 | localStorage: client1Storage,
24 | logger: nil
25 | )
26 | )
27 |
28 | let client2 = AuthClient(
29 | configuration: AuthClient.Configuration(
30 | url: url,
31 | localStorage: client2Storage,
32 | logger: nil
33 | )
34 | )
35 |
36 | XCTAssertNotEqual(client1.clientID, client2.clientID)
37 |
38 | XCTAssertIdentical(
39 | Dependencies[client1.clientID].configuration.localStorage as? InMemoryLocalStorage,
40 | client1Storage
41 | )
42 | XCTAssertIdentical(
43 | Dependencies[client2.clientID].configuration.localStorage as? InMemoryLocalStorage,
44 | client2Storage
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/AuthTests/Resources/anonymous-sign-in-response.json:
--------------------------------------------------------------------------------
1 | {
2 | "access_token" : "eyJhbGciOiJIUzI1NiIsImtpZCI6ImpIaU1GZmtNTzRGdVROdXUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzExOTk0NzEzLCJpYXQiOjE3MTE5OTExMTMsImlzcyI6Imh0dHBzOi8vYWp5YWdzaHV6bnV2anFoampmdG8uc3VwYWJhc2UuY28vYXV0aC92MSIsInN1YiI6ImJiZmE5MjU0LWM1ZDEtNGNmZi1iYTc2LTU2YmYwM2IwNWEwMSIsImVtYWlsIjoiIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnt9LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJhbm9ueW1vdXMiLCJ0aW1lc3RhbXAiOjE3MTE5OTExMTN9XSwic2Vzc2lvbl9pZCI6ImMyODlmYTcwLWIzYWUtNDI1Yi05MDQxLWUyZjVhNzBlZTcyYSIsImlzX2Fub255bW91cyI6dHJ1ZX0.whBzmyMv3-AQSaiY6Fi-v_G68Q8oULhB7axImj9qOdw",
3 | "expires_at" : 1711994713,
4 | "expires_in" : 3600,
5 | "refresh_token" : "0xS9iJUWdXnWlCJtFiXk5A",
6 | "token_type" : "bearer",
7 | "user" : {
8 | "app_metadata" : {
9 |
10 | },
11 | "aud" : "authenticated",
12 | "created_at" : "2024-04-01T17:05:13.013312Z",
13 | "email" : "",
14 | "id" : "bbfa9254-c5d1-4cff-ba76-56bf03b05a01",
15 | "identities" : [
16 |
17 | ],
18 | "is_anonymous" : true,
19 | "last_sign_in_at" : "2024-04-01T17:05:13.018294975Z",
20 | "phone" : "",
21 | "role" : "authenticated",
22 | "updated_at" : "2024-04-01T17:05:13.022041Z",
23 | "user_metadata" : {
24 |
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Examples/UserManagement/AvatarImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AvatarImage.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if canImport(UIKit)
11 | typealias PlatformImage = UIImage
12 | extension Image {
13 | init(platformImage: PlatformImage) {
14 | self.init(uiImage: platformImage)
15 | }
16 | }
17 |
18 | #elseif canImport(AppKit)
19 | typealias PlatformImage = NSImage
20 | extension Image {
21 | init(platformImage: PlatformImage) {
22 | self.init(nsImage: platformImage)
23 | }
24 | }
25 | #endif
26 |
27 | struct AvatarImage: Transferable, Equatable {
28 | let image: Image
29 | let data: Data
30 |
31 | static var transferRepresentation: some TransferRepresentation {
32 | DataRepresentation(importedContentType: .image) { data in
33 | guard let image = await AvatarImage(data: data) else {
34 | throw TransferError.importFailed
35 | }
36 |
37 | return image
38 | }
39 | }
40 | }
41 |
42 | extension AvatarImage {
43 | @MainActor
44 | init?(data: Data) {
45 | guard let uiImage = PlatformImage(data: data) else {
46 | return nil
47 | }
48 |
49 | let image = Image(platformImage: uiImage)
50 | self.init(image: image, data: data)
51 | }
52 | }
53 |
54 | enum TransferError: Error {
55 | case importFailed
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/AuthTests/Resources/local-storage.json:
--------------------------------------------------------------------------------
1 | {
2 | "supabase.auth.token" : {
3 | "accessToken" : "accesstoken",
4 | "expiresAt" : 1711977907,
5 | "expiresIn" : 120,
6 | "refreshToken" : "refreshtoken",
7 | "tokenType" : "bearer",
8 | "user" : {
9 | "appMetadata" : {
10 | "provider" : "email",
11 | "providers" : [
12 | "email"
13 | ]
14 | },
15 | "aud" : "authenticated",
16 | "confirmationSentAt" : 671198221,
17 | "createdAt" : 671198221,
18 | "email" : "johndoe@supabsae.com",
19 | "id" : "859F402D-B3DE-4105-A1B9-932836D9193B",
20 | "identities" : [
21 | {
22 | "createdAt" : 671198221,
23 | "id" : "859f402d-b3de-4105-a1b9-932836d9193b",
24 | "identityData" : {
25 | "sub" : "859f402d-b3de-4105-a1b9-932836d9193b"
26 | },
27 | "identityId" : "859F402D-B3DE-4105-A1B9-932836D9193B",
28 | "lastSignInAt" : 671198221,
29 | "provider" : "email",
30 | "updatedAt" : 671198221,
31 | "userId" : "859F402D-B3DE-4105-A1B9-932836D9193B"
32 | }
33 | ],
34 | "isAnonymous" : false,
35 | "phone" : "",
36 | "role" : "authenticated",
37 | "updatedAt" : 671198221,
38 | "userMetadata" : {
39 | "referrer_id" : null
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/Examples/Examples/Auth/GoogleSignInSDKFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GoogleSignInSDKFlow.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 05/03/24.
6 | //
7 |
8 | import GoogleSignIn
9 | import GoogleSignInSwift
10 | import Supabase
11 | import SwiftUI
12 |
13 | @MainActor
14 | struct GoogleSignInSDKFlow: View {
15 | var body: some View {
16 | GoogleSignInButton(action: handleSignIn)
17 | }
18 |
19 | func handleSignIn() {
20 | Task {
21 | do {
22 | let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: root)
23 |
24 | guard let idToken = result.user.idToken?.tokenString else {
25 | debug("No 'idToken' returned by GIDSignIn call.")
26 | return
27 | }
28 |
29 | try await supabase.auth.signInWithIdToken(
30 | credentials: OpenIDConnectCredentials(
31 | provider: .google,
32 | idToken: idToken
33 | )
34 | )
35 | } catch {
36 | debug("GoogleSignIn failed: \(error)")
37 | }
38 | }
39 | }
40 |
41 | #if canImport(UIKit)
42 | var root: UIViewController {
43 | UIApplication.shared.firstKeyWindow?.rootViewController ?? UIViewController()
44 | }
45 | #else
46 | var root: NSWindow {
47 | NSApplication.shared.keyWindow ?? NSWindow()
48 | }
49 | #endif
50 | }
51 |
52 | #Preview {
53 | GoogleSignInSDKFlow()
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/AuthTests/ExtractParamsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtractParamsTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 23/12/23.
6 | //
7 |
8 | import XCTest
9 |
10 | @testable import Auth
11 |
12 | final class ExtractParamsTests: XCTestCase {
13 | func testExtractParamsInQuery() {
14 | let code = UUID().uuidString
15 | let url = URL(string: "io.supabase.flutterquickstart://login-callback/?code=\(code)")!
16 | let params = extractParams(from: url)
17 | XCTAssertEqual(params, ["code": code])
18 | }
19 |
20 | func testExtractParamsInFragment() {
21 | let code = UUID().uuidString
22 | let url = URL(string: "io.supabase.flutterquickstart://login-callback/#code=\(code)")!
23 | let params = extractParams(from: url)
24 | XCTAssertEqual(params, ["code": code])
25 | }
26 |
27 | func testExtractParamsInBothFragmentAndQuery() {
28 | let code = UUID().uuidString
29 | let url = URL(
30 | string: "io.supabase.flutterquickstart://login-callback/?code=\(code)#message=abc")!
31 | let params = extractParams(from: url)
32 | XCTAssertEqual(params, ["code": code, "message": "abc"])
33 | }
34 |
35 | func testExtractParamsQueryTakesPrecedence() {
36 | let url = URL(string: "io.supabase.flutterquickstart://login-callback/?code=123#code=abc")!
37 | let params = extractParams(from: url)
38 | XCTAssertEqual(params, ["code": "123"])
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Examples/Examples/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLSchemes
11 |
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 |
14 |
15 |
16 | CFBundleTypeRole
17 | Editor
18 | CFBundleURLSchemes
19 |
20 | {{ DOT_REVERSED_IOS_CLIENT_ID }}
21 |
22 |
23 |
24 | CFBundleURLSchemes
25 |
26 | fb{{ FACEBOOK APP ID }}
27 |
28 |
29 |
30 | GIDClientID
31 | {{ YOUR_IOS_CLIENT_ID }}
32 | GIDServerClientID
33 | {{ YOUR_SERVER_CLIENT_ID }}
34 | FacebookAppID
35 | {{ FACEBOOK APP ID }}
36 | FacebookClientToken
37 | {{ FACEBOOK CLIENT TOKEN }}
38 | FacebookDisplayName
39 | Examples
40 | LSApplicationQueriesSchemes
41 |
42 | fbapi
43 | fb-messenger-share-api
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Sources/Helpers/Codable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Codable.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 20/01/25.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 | import XCTestDynamicOverlay
11 |
12 | extension JSONDecoder {
13 | /// Default `JSONDecoder` for decoding types from Supabase.
14 | package static func supabase() -> JSONDecoder {
15 | let decoder = JSONDecoder()
16 | decoder.dateDecodingStrategy = .custom { decoder in
17 | let container = try decoder.singleValueContainer()
18 | let string = try container.decode(String.self)
19 |
20 | if let date = string.date {
21 | return date
22 | }
23 |
24 | throw DecodingError.dataCorruptedError(
25 | in: container,
26 | debugDescription: "Invalid date format: \(string)"
27 | )
28 | }
29 | return decoder
30 | }
31 | }
32 | extension JSONEncoder {
33 | /// Default `JSONEncoder` for encoding types to Supabase.
34 | package static func supabase() -> JSONEncoder {
35 | let encoder = JSONEncoder()
36 | encoder.dateEncodingStrategy = .custom { date, encoder in
37 | var container = encoder.singleValueContainer()
38 | let string = date.iso8601String
39 | try container.encode(string)
40 | }
41 |
42 | #if DEBUG
43 | if isTesting {
44 | encoder.outputFormatting = [.sortedKeys]
45 | }
46 | #endif
47 |
48 | return encoder
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Examples/Examples/Shared/GitHubSourceLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitHubSourceLink.swift
3 | // Examples
4 | //
5 | // Helper for generating GitHub source code links
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct GitHubSourceLink {
12 | static let baseURL = URL(
13 | string: "https://github.com/supabase/supabase-swift/blob/main"
14 | )!
15 |
16 | static func url(for file: String = #file) -> URL {
17 | let paths = file.split(separator: "/")
18 |
19 | guard let rootIndex = paths.firstIndex(where: { $0 == "Examples" }) else {
20 | return baseURL
21 | }
22 |
23 | let relativePath = paths[rootIndex...].joined(separator: "/")
24 | return baseURL.appendingPathComponent(relativePath)
25 | }
26 | }
27 |
28 | struct GitHubSourceLinkViewModifier: ViewModifier {
29 | @Environment(\.openURL) var openURL
30 |
31 | let file: String
32 |
33 | func body(content: Content) -> some View {
34 | content
35 | .toolbar {
36 | ToolbarItem(placement: .primaryAction) {
37 | Button {
38 | openURL(GitHubSourceLink.url(for: file))
39 | } label: {
40 | Label("View Source", systemImage: "chevron.left.forwardslash.chevron.right")
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | extension View {
48 | func gitHubSourceLink(for file: String = #file) -> some View {
49 | modifier(GitHubSourceLinkViewModifier(file: file))
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/TestHelpers/MockExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockExtensions.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 21/01/25.
6 | //
7 |
8 | import Mocker
9 | import Foundation
10 | import InlineSnapshotTesting
11 |
12 | extension Mock {
13 | package func snapshotRequest(
14 | message: @autoclosure () -> String = "",
15 | record isRecording: SnapshotTestingConfiguration.Record? = nil,
16 | timeout: TimeInterval = 5,
17 | syntaxDescriptor: InlineSnapshotSyntaxDescriptor = InlineSnapshotSyntaxDescriptor(),
18 | matches expected: (() -> String)? = nil,
19 | fileID: StaticString = #fileID,
20 | file filePath: StaticString = #filePath,
21 | function: StaticString = #function,
22 | line: UInt = #line,
23 | column: UInt = #column
24 | ) -> Self {
25 | #if os(Linux) || os(Android)
26 | // non-Darwin curl snapshots have a different Content-Length than expected
27 | return self
28 | #endif
29 | var copy = self
30 | copy.onRequestHandler = OnRequestHandler {
31 | assertInlineSnapshot(
32 | of: $0,
33 | as: ._curl,
34 | record: isRecording,
35 | timeout: timeout,
36 | syntaxDescriptor: syntaxDescriptor,
37 | matches: expected,
38 | fileID: fileID,
39 | file: filePath,
40 | function: function,
41 | line: line,
42 | column: column
43 | )
44 | }
45 | return copy
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/PostgrestFilterValueTests.swift:
--------------------------------------------------------------------------------
1 | import PostgREST
2 | import XCTest
3 |
4 | final class PostgrestFilterValue: XCTestCase {
5 | func testArray() {
6 | let array = ["is:online", "faction:red"]
7 | let queryValue = array.rawValue
8 | XCTAssertEqual(queryValue, "{is:online,faction:red}")
9 | }
10 |
11 | func testAnyJSON() {
12 | XCTAssertEqual(
13 | AnyJSON.array(["is:online", "faction:red"]).rawValue,
14 | "{is:online,faction:red}"
15 | )
16 | XCTAssertEqual(
17 | AnyJSON.object(["postalcode": 90210]).rawValue,
18 | "{\"postalcode\":90210}"
19 | )
20 | XCTAssertEqual(AnyJSON.string("string").rawValue, "string")
21 | XCTAssertEqual(AnyJSON.double(3.14).rawValue, "3.14")
22 | XCTAssertEqual(AnyJSON.integer(3).rawValue, "3")
23 | XCTAssertEqual(AnyJSON.bool(true).rawValue, "true")
24 | XCTAssertEqual(AnyJSON.null.rawValue, "NULL")
25 | }
26 |
27 | func testOptional() {
28 | XCTAssertEqual(Optional.some([1, 2]).rawValue, "{1,2}")
29 | XCTAssertEqual(Optional<[Int]>.none.rawValue, "NULL")
30 | }
31 |
32 | func testUUID() {
33 | XCTAssertEqual(
34 | UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!.rawValue,
35 | "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")
36 | }
37 |
38 | func testDate() {
39 | XCTAssertEqual(
40 | Date(timeIntervalSince1970: 1_737_465_985).rawValue,
41 | "2025-01-21T13:26:25.000Z"
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Realtime/RealtimePostgresFilter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimePostgresFilter.swift
3 | // Supabase
4 | //
5 | // Created by Lucas Abijmil on 19/02/2025.
6 | //
7 |
8 | /// A filter that can be used in Realtime.
9 | public enum RealtimePostgresFilter {
10 | case eq(_ column: String, value: any RealtimePostgresFilterValue)
11 | case neq(_ column: String, value: any RealtimePostgresFilterValue)
12 | case gt(_ column: String, value: any RealtimePostgresFilterValue)
13 | case gte(_ column: String, value: any RealtimePostgresFilterValue)
14 | case lt(_ column: String, value: any RealtimePostgresFilterValue)
15 | case lte(_ column: String, value: any RealtimePostgresFilterValue)
16 | case `in`(_ column: String, values: [any RealtimePostgresFilterValue])
17 |
18 | var value: String {
19 | switch self {
20 | case let .eq(column, value):
21 | return "\(column)=eq.\(value.rawValue)"
22 | case let .neq(column, value):
23 | return "\(column)=neq.\(value.rawValue)"
24 | case let .gt(column, value):
25 | return "\(column)=gt.\(value.rawValue)"
26 | case let .gte(column, value):
27 | return "\(column)=gte.\(value.rawValue)"
28 | case let .lt(column, value):
29 | return "\(column)=lt.\(value.rawValue)"
30 | case let .lte(column, value):
31 | return "\(column)=lte.\(value.rawValue)"
32 | case let .in(column, values):
33 | return "\(column)=in.(\(values.map(\.rawValue).joined(separator: ",")))"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Helpers/TaskLocalHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskLocalHelpers.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 29/05/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if compiler(>=6.0)
11 | extension TaskLocal where Value == JSONObject {
12 | @discardableResult
13 | @inlinable package final func withValue(
14 | merging valueDuringOperation: Value,
15 | operation: () async throws -> R,
16 | isolation: isolated (any Actor)? = #isolation,
17 | file: String = #fileID,
18 | line: UInt = #line
19 | ) async rethrows -> R {
20 | let currentValue = wrappedValue
21 | return try await withValue(
22 | currentValue.merging(valueDuringOperation) { _, new in new },
23 | operation: operation,
24 | isolation: isolation,
25 | file: file,
26 | line: line
27 | )
28 | }
29 | }
30 | #else
31 | extension TaskLocal where Value == JSONObject {
32 | @_unsafeInheritExecutor
33 | @discardableResult
34 | @inlinable package final func withValue(
35 | merging valueDuringOperation: Value,
36 | operation: () async throws -> R,
37 | file: String = #fileID,
38 | line: UInt = #line
39 | ) async rethrows -> R {
40 | let currentValue = wrappedValue
41 | return try await withValue(
42 | currentValue.merging(valueDuringOperation) { _, new in new },
43 | operation: operation,
44 | file: file,
45 | line: line
46 | )
47 | }
48 | }
49 | #endif
50 |
--------------------------------------------------------------------------------
/Tests/AuthTests/Resources/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "859f402d-b3de-4105-a1b9-932836d9193b",
3 | "aud": "authenticated",
4 | "role": "authenticated",
5 | "email": "guilherme@grds.dev",
6 | "phone": "",
7 | "confirmation_sent_at": "2022-04-09T11:57:01.710600634Z",
8 | "app_metadata": {
9 | "provider": "email",
10 | "providers": [
11 | "email"
12 | ]
13 | },
14 | "user_metadata": {
15 | "referrer_id": null
16 | },
17 | "identities": [
18 | {
19 | "id": "859f402d-b3de-4105-a1b9-932836d9193b",
20 | "user_id": "859f402d-b3de-4105-a1b9-932836d9193b",
21 | "identity_id": "859f402d-b3de-4105-a1b9-932836d9193b",
22 | "identity_data": {
23 | "sub": "859f402d-b3de-4105-a1b9-932836d9193b"
24 | },
25 | "provider": "email",
26 | "last_sign_in_at": "2022-04-09T11:23:45Z",
27 | "created_at": "2022-04-09T11:23:45.899924Z",
28 | "updated_at": "2022-04-09T11:23:45.899926Z"
29 | },
30 | {
31 | "id": "6c69f399-be1b-467a-9c53-d3753abdd2df",
32 | "user_id": "6c69f399-be1b-467a-9c53-d3753abdd2df",
33 | "identity_id": "6c69f399-be1b-467a-9c53-d3753abdd2df",
34 | "identity_data": {
35 | "sub": "6c69f399-be1b-467a-9c53-d3753abdd2df"
36 | },
37 | "provider": "github",
38 | "created_at": "2022-04-09T11:23:45.899924Z",
39 | "updated_at": "2022-04-09T11:23:45.899926Z"
40 | }
41 | ],
42 | "created_at": "2022-04-09T11:23:45.874827Z",
43 | "updated_at": "2022-04-09T11:57:01.720803Z"
44 | }
45 |
--------------------------------------------------------------------------------
/Examples/SlackClone/ChannelListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChannelListView.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 27/12/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ChannelListView: View {
11 | @Bindable var store = Dependencies.shared.channel
12 | @Binding var channel: Channel?
13 |
14 | @State private var addChannelPresented = false
15 | @State private var newChannelName = ""
16 |
17 | var body: some View {
18 | List(store.channels, selection: $channel) { channel in
19 | NavigationLink(channel.slug, value: channel)
20 | }
21 | .toolbar {
22 | ToolbarItem(placement: .primaryAction) {
23 | Button("Add Channel") {
24 | addChannelPresented = true
25 | }
26 | .popover(isPresented: $addChannelPresented) {
27 | addChannelView
28 | }
29 | }
30 | ToolbarItem {
31 | Button("Log out") {
32 | Task {
33 | try? await supabase.auth.signOut()
34 | }
35 | }
36 | }
37 | }
38 | .toast(state: $store.toast)
39 | }
40 |
41 | private var addChannelView: some View {
42 | Form {
43 | Section {
44 | TextField("New channel name", text: $newChannelName)
45 | }
46 |
47 | Section {
48 | Button("Add") {
49 | Task {
50 | await store.addChannel(newChannelName)
51 | addChannelPresented = false
52 | }
53 | }
54 | }
55 | }
56 | #if os(macOS)
57 | .padding()
58 | #endif
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Examples/Examples/ActionState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActionState.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 15/12/23.
6 | //
7 |
8 | import CasePaths
9 | import Foundation
10 | import SwiftUI
11 |
12 | @CasePathable
13 | enum ActionState {
14 | case idle
15 | case inFlight
16 | case result(Result)
17 |
18 | var success: Success? {
19 | if case .result(.success(let success)) = self { return success }
20 | return nil
21 | }
22 | }
23 |
24 | struct ActionStateView: View {
25 | @Binding var state: ActionState
26 |
27 | let action: () async throws -> Success
28 | @ViewBuilder var content: (Success) -> SuccessContent
29 |
30 | var body: some View {
31 | Group {
32 | switch state {
33 | case .idle:
34 | Color.clear
35 | case .inFlight:
36 | ProgressView()
37 | case .result(.success(let value)):
38 | content(value)
39 | case .result(.failure(let error)):
40 | VStack {
41 | ErrorText(error)
42 | Button("Retry") {
43 | Task { await load() }
44 | }
45 | }
46 | }
47 | }
48 | .task {
49 | await load()
50 | }
51 | }
52 |
53 | @MainActor
54 | private func load() async {
55 | state = .inFlight
56 | do {
57 | let value = try await action()
58 | state = .result(.success(value))
59 | } catch {
60 | state = .result(.failure(error))
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Examples/SlackClone/AuthView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthView.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 27/12/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @Observable
11 | @MainActor
12 | final class AuthViewModel {
13 | var email = ""
14 | var toast: ToastState?
15 |
16 | func signInButtonTapped() async {
17 | do {
18 | try await supabase.auth.signInWithOTP(email: email)
19 | toast = ToastState(status: .success, title: "Check your inbox.")
20 |
21 | try? await Task.sleep(for: .seconds(1))
22 |
23 | #if os(macOS)
24 | NSWorkspace.shared.open(URL(string: "http://127.0.0.1:54324")!)
25 | #else
26 | await UIApplication.shared.open(URL(string: "http://127.0.0.1:54324")!)
27 | #endif
28 | } catch {
29 | toast = ToastState(status: .error, title: "Error", description: error.localizedDescription)
30 | }
31 | }
32 | }
33 |
34 | struct AuthView: View {
35 | @Bindable var model = AuthViewModel()
36 |
37 | var body: some View {
38 | VStack {
39 | VStack {
40 | TextField("Email", text: $model.email)
41 | #if os(iOS)
42 | .textInputAutocapitalization(.never)
43 | .keyboardType(.emailAddress)
44 | #endif
45 | .textContentType(.emailAddress)
46 | .autocorrectionDisabled()
47 | }
48 | Button("Sign in with Magic Link") {
49 | Task { await model.signInButtonTapped() }
50 | }
51 | }
52 | .padding()
53 | .toast(state: $model.toast)
54 | }
55 | }
56 |
57 | #Preview {
58 | AuthView()
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Auth/Internal/CodeVerifierStorage.swift:
--------------------------------------------------------------------------------
1 | import ConcurrencyExtras
2 | import Foundation
3 |
4 | struct CodeVerifierStorage: Sendable {
5 | var get: @Sendable () -> String?
6 | var set: @Sendable (_ code: String?) -> Void
7 | }
8 |
9 | extension CodeVerifierStorage {
10 | static func live(clientID: AuthClientID) -> Self {
11 | var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
12 | var key: String { "\(configuration.storageKey ?? defaultStorageKey)-code-verifier" }
13 |
14 | return Self(
15 | get: {
16 | do {
17 | guard let data = try configuration.localStorage.retrieve(key: key) else {
18 | configuration.logger?.debug("Code verifier not found.")
19 | return nil
20 | }
21 | return String(decoding: data, as: UTF8.self)
22 | } catch {
23 | configuration.logger?.error("Failure loading code verifier: \(error.localizedDescription)")
24 | return nil
25 | }
26 | },
27 | set: { code in
28 | do {
29 | if let code, let data = code.data(using: .utf8) {
30 | try configuration.localStorage.store(key: key, value: data)
31 | } else if code == nil {
32 | try configuration.localStorage.remove(key: key)
33 | } else {
34 | configuration.logger?.error("Code verifier is not a valid UTF8 string.")
35 | }
36 | } catch {
37 | configuration.logger?.error("Failure storing code verifier: \(error.localizedDescription)")
38 | }
39 | }
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/PostgresQueryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgrestQueryTests.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 21/01/25.
6 | //
7 |
8 | import InlineSnapshotTesting
9 | import Mocker
10 | import PostgREST
11 | import TestHelpers
12 | import XCTest
13 |
14 | #if canImport(FoundationNetworking)
15 | import FoundationNetworking
16 | #endif
17 |
18 | class PostgrestQueryTests: XCTestCase {
19 | let url = URL(string: "http://localhost:54321/rest/v1")!
20 |
21 | let sessionConfiguration: URLSessionConfiguration = {
22 | let configuration = URLSessionConfiguration.default
23 | configuration.protocolClasses = [MockingURLProtocol.self]
24 | return configuration
25 | }()
26 |
27 | lazy var session = URLSession(configuration: sessionConfiguration)
28 |
29 | lazy var sut = PostgrestClient(
30 | url: url,
31 | headers: [
32 | "apikey":
33 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
34 | ],
35 | logger: nil,
36 | fetch: {
37 | try await self.session.data(for: $0)
38 | },
39 | encoder: {
40 | let encoder = PostgrestClient.Configuration.jsonEncoder
41 | encoder.outputFormatting = [.sortedKeys]
42 | return encoder
43 | }()
44 | )
45 |
46 | struct User: Codable {
47 | let id: Int
48 | let username: String
49 | }
50 |
51 | struct Country: Decodable {
52 | let name: String
53 | let cities: [City]
54 |
55 | struct City: Decodable {
56 | let name: String
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Helpers/_Clock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // _Clock.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 08/01/25.
6 | //
7 |
8 | import Clocks
9 | import ConcurrencyExtras
10 | import Foundation
11 |
12 | package protocol _Clock: Sendable {
13 | func sleep(for duration: TimeInterval) async throws
14 | }
15 |
16 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
17 | extension ContinuousClock: _Clock {
18 | package func sleep(for duration: TimeInterval) async throws {
19 | try await sleep(for: .seconds(duration))
20 | }
21 | }
22 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
23 | extension TestClock: _Clock {
24 | package func sleep(for duration: TimeInterval) async throws {
25 | try await sleep(for: .seconds(duration))
26 | }
27 | }
28 |
29 | /// `_Clock` used on platforms where ``Clock`` protocol isn't available.
30 | struct FallbackClock: _Clock {
31 | func sleep(for duration: TimeInterval) async throws {
32 | try await Task.sleep(nanoseconds: NSEC_PER_SEC * UInt64(duration))
33 | }
34 | }
35 |
36 | // Resolves clock instance based on platform availability.
37 | let _resolveClock: @Sendable () -> any _Clock = {
38 | if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
39 | ContinuousClock()
40 | } else {
41 | FallbackClock()
42 | }
43 | }
44 |
45 | private let __clock = LockIsolated(_resolveClock())
46 |
47 | #if DEBUG
48 | package var _clock: any _Clock {
49 | get {
50 | __clock.value
51 | }
52 | set {
53 | __clock.setValue(newValue)
54 | }
55 | }
56 | #else
57 | package var _clock: any _Clock {
58 | __clock.value
59 | }
60 | #endif
61 |
--------------------------------------------------------------------------------
/Examples/SlackClone/AppView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppView.swift
3 | // SlackClone
4 | //
5 | // Created by Guilherme Souza on 27/12/23.
6 | //
7 |
8 | import OSLog
9 | import Supabase
10 | import SwiftUI
11 |
12 | @Observable
13 | @MainActor
14 | final class AppViewModel {
15 | var session: Session?
16 | var selectedChannel: Channel?
17 |
18 | var realtimeConnectionStatus: RealtimeClientStatus?
19 |
20 | init() {
21 | Task {
22 | for await (event, session) in supabase.auth.authStateChanges {
23 | Logger.main.debug("AuthStateChange: \(event.rawValue)")
24 | guard [.signedIn, .signedOut, .initialSession, .tokenRefreshed].contains(event) else {
25 | return
26 | }
27 | self.session = session
28 |
29 | if session == nil {
30 | for subscription in supabase.channels {
31 | await subscription.unsubscribe()
32 | }
33 | }
34 | }
35 | }
36 |
37 | Task {
38 | for await status in supabase.realtimeV2.statusChange {
39 | realtimeConnectionStatus = status
40 | }
41 | }
42 | }
43 | }
44 |
45 | struct AppView: View {
46 | @Bindable var model: AppViewModel
47 | @State var logPresented = false
48 |
49 | @ViewBuilder
50 | var body: some View {
51 | if model.session != nil {
52 | NavigationSplitView {
53 | ChannelListView(channel: $model.selectedChannel)
54 | } detail: {
55 | if let channel = model.selectedChannel {
56 | MessagesView(channel: channel).id(channel.id)
57 | }
58 | }
59 | } else {
60 | AuthView()
61 | }
62 | }
63 | }
64 |
65 | #Preview {
66 | AppView(model: AppViewModel())
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/Realtime/PushV2.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PushV2.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 02/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Represents the different status of a push
11 | public enum PushStatus: String, Sendable {
12 | case ok
13 | case error
14 | case timeout
15 | }
16 |
17 | @MainActor
18 | final class PushV2 {
19 | private weak var channel: (any RealtimeChannelProtocol)?
20 | let message: RealtimeMessageV2
21 |
22 | private var receivedContinuation: CheckedContinuation?
23 |
24 | init(channel: (any RealtimeChannelProtocol)?, message: RealtimeMessageV2) {
25 | self.channel = channel
26 | self.message = message
27 | }
28 |
29 | func send() async -> PushStatus {
30 | guard let channel = channel else {
31 | return .error
32 | }
33 |
34 | channel.socket.push(message)
35 |
36 | if !channel.config.broadcast.acknowledgeBroadcasts {
37 | // channel was configured with `ack = false`,
38 | // don't wait for a response and return `ok`.
39 | return .ok
40 | }
41 |
42 | do {
43 | return try await withTimeout(interval: channel.socket.options.timeoutInterval) {
44 | await withCheckedContinuation { continuation in
45 | self.receivedContinuation = continuation
46 | }
47 | }
48 | } catch is TimeoutError {
49 | channel.logger?.debug("Push timed out.")
50 | return .timeout
51 | } catch {
52 | channel.logger?.error("Error sending push: \(error.localizedDescription)")
53 | return .error
54 | }
55 | }
56 |
57 | func didReceive(status: PushStatus) {
58 | receivedContinuation?.resume(returning: status)
59 | receivedContinuation = nil
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/HelpersTests/EventEmitterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EventEmitterTests.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 15/10/24.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Helpers
10 | import XCTest
11 |
12 | final class EventEmitterTests: XCTestCase {
13 |
14 | func testBasics() {
15 | let sut = EventEmitter(initialEvent: "0")
16 | XCTAssertTrue(sut.emitsLastEventWhenAttaching)
17 |
18 | XCTAssertEqual(sut.lastEvent, "0")
19 |
20 | let receivedEvents = LockIsolated<[String]>([])
21 |
22 | let tokenA = sut.attach { value in
23 | receivedEvents.withValue { $0.append("a" + value) }
24 | }
25 |
26 | let tokenB = sut.attach { value in
27 | receivedEvents.withValue { $0.append("b" + value) }
28 | }
29 |
30 | sut.emit("1")
31 | sut.emit("2")
32 | sut.emit("3")
33 | sut.emit("4")
34 |
35 | sut.emit("5", to: tokenA)
36 | sut.emit("6", to: tokenB)
37 |
38 | tokenA.cancel()
39 |
40 | sut.emit("7")
41 | sut.emit("8")
42 |
43 | XCTAssertEqual(sut.lastEvent, "8")
44 |
45 | XCTAssertEqual(
46 | receivedEvents.value,
47 | ["a0", "b0", "a1", "b1", "a2", "b2", "a3", "b3", "a4", "b4", "a5", "b6", "b7", "b8"]
48 | )
49 | }
50 |
51 | func test_dontEmitLastEventWhenAttaching() {
52 | let sut = EventEmitter(initialEvent: "0", emitsLastEventWhenAttaching: false)
53 | XCTAssertFalse(sut.emitsLastEventWhenAttaching)
54 |
55 | let receivedEvent = LockIsolated<[String]>([])
56 | let token = sut.attach { value in
57 | receivedEvent.withValue { $0.append(value) }
58 | }
59 |
60 | sut.emit("1")
61 |
62 | XCTAssertEqual(receivedEvent.value, ["1"])
63 |
64 | token.cancel()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/AuthTests/Resources/session.json:
--------------------------------------------------------------------------------
1 | {
2 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6Imd1aWxoZXJtZTJAZ3Jkcy5kZXYiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.4lMvmz2pJkWu1hMsBgXP98Fwz4rbvFYl4VA9joRv6kY",
3 | "token_type": "bearer",
4 | "expires_in": 3600,
5 | "expires_at": 345345345,
6 | "refresh_token": "GGduTeu95GraIXQ56jppkw",
7 | "user": {
8 | "id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8",
9 | "aud": "authenticated",
10 | "role": "authenticated",
11 | "email": "guilherme@binaryscraping.co",
12 | "email_confirmed_at": "2022-03-30T10:33:41.018575157Z",
13 | "phone": "",
14 | "last_sign_in_at": "2022-03-30T10:33:41.021531328Z",
15 | "app_metadata": {
16 | "provider": "email",
17 | "providers": [
18 | "email"
19 | ]
20 | },
21 | "user_metadata": {},
22 | "identities": [
23 | {
24 | "id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8",
25 | "user_id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8",
26 | "identity_id": "859f402d-b3de-4105-a1b9-932836d9193b",
27 | "identity_data": {
28 | "sub": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8"
29 | },
30 | "provider": "email",
31 | "last_sign_in_at": "2022-03-30T10:33:41.015557063Z",
32 | "created_at": "2022-03-30T10:33:41.015612Z",
33 | "updated_at": "2022-03-30T10:33:41.015616Z"
34 | }
35 | ],
36 | "created_at": "2022-03-30T10:33:41.005433Z",
37 | "updated_at": "2022-03-30T10:33:41.022688Z"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/RealtimeTests/RealtimeErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimeErrorTests.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 29/07/25.
6 | //
7 |
8 | import XCTest
9 |
10 | @testable import Realtime
11 |
12 | final class RealtimeErrorTests: XCTestCase {
13 | func testRealtimeErrorInitialization() {
14 | let errorMessage = "Connection failed"
15 | let error = RealtimeError(errorMessage)
16 |
17 | XCTAssertEqual(error.errorDescription, errorMessage)
18 | }
19 |
20 | func testRealtimeErrorLocalizedDescription() {
21 | let errorMessage = "Test error message"
22 | let error = RealtimeError(errorMessage)
23 |
24 | // LocalizedError protocol provides localizedDescription
25 | XCTAssertEqual(error.localizedDescription, errorMessage)
26 | }
27 |
28 | func testRealtimeErrorWithEmptyMessage() {
29 | let error = RealtimeError("")
30 | XCTAssertEqual(error.errorDescription, "")
31 | }
32 |
33 | func testRealtimeErrorAsError() {
34 | let errorMessage = "Network timeout"
35 | let realtimeError = RealtimeError(errorMessage)
36 | let error: Error = realtimeError
37 |
38 | // Test that it can be used as a general Error
39 | XCTAssertNotNil(error)
40 | XCTAssertEqual(error.localizedDescription, errorMessage)
41 | }
42 |
43 | func testRealtimeErrorEquality() {
44 | let error1 = RealtimeError("Same message")
45 | let error2 = RealtimeError("Same message")
46 | let error3 = RealtimeError("Different message")
47 |
48 | // Since RealtimeError doesn't implement Equatable, we test the description
49 | XCTAssertEqual(error1.errorDescription, error2.errorDescription)
50 | XCTAssertNotEqual(error1.errorDescription, error3.errorDescription)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Helpers/HTTP/HTTPClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPClient.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 30/04/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(FoundationNetworking)
11 | import FoundationNetworking
12 | #endif
13 |
14 | package protocol HTTPClientType: Sendable {
15 | func send(_ request: HTTPRequest) async throws -> HTTPResponse
16 | }
17 |
18 | package actor HTTPClient: HTTPClientType {
19 | let fetch: @Sendable (URLRequest) async throws -> (Data, URLResponse)
20 | let interceptors: [any HTTPClientInterceptor]
21 |
22 | package init(
23 | fetch: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse),
24 | interceptors: [any HTTPClientInterceptor]
25 | ) {
26 | self.fetch = fetch
27 | self.interceptors = interceptors
28 | }
29 |
30 | package func send(_ request: HTTPRequest) async throws -> HTTPResponse {
31 | var next: @Sendable (HTTPRequest) async throws -> HTTPResponse = { _request in
32 | let urlRequest = _request.urlRequest
33 | let (data, response) = try await self.fetch(urlRequest)
34 | guard let httpURLResponse = response as? HTTPURLResponse else {
35 | throw URLError(.badServerResponse)
36 | }
37 | return HTTPResponse(data: data, response: httpURLResponse)
38 | }
39 |
40 | for interceptor in interceptors.reversed() {
41 | let tmp = next
42 | next = {
43 | try await interceptor.intercept($0, next: tmp)
44 | }
45 | }
46 |
47 | return try await next(request)
48 | }
49 | }
50 |
51 | package protocol HTTPClientInterceptor: Sendable {
52 | func intercept(
53 | _ request: HTTPRequest,
54 | next: @Sendable (HTTPRequest) async throws -> HTTPResponse
55 | ) async throws -> HTTPResponse
56 | }
57 |
--------------------------------------------------------------------------------
/scripts/check-for-breaking-api-changes.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | ##===----------------------------------------------------------------------===##
3 | ##
4 | ## This source file is part of the SwiftOpenAPIGenerator open source project
5 | ##
6 | ## Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
7 | ## Licensed under Apache License v2.0
8 | ##
9 | ## See LICENSE.txt for license information
10 | ## See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
11 | ##
12 | ## SPDX-License-Identifier: Apache-2.0
13 | ##
14 | ##===----------------------------------------------------------------------===##
15 |
16 | set -euo pipefail
17 |
18 | log() { printf -- "** %s\n" "$*" >&2; }
19 | error() { printf -- "** ERROR: %s\n" "$*" >&2; }
20 | fatal() { error "$@"; exit 1; }
21 |
22 | CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
23 | REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)"
24 |
25 | log "Checking required environment variables..."
26 | test -n "${BASELINE_REPO_URL:-}" || fatal "BASELINE_REPO_URL unset"
27 | test -n "${BASELINE_TREEISH:-}" || fatal "BASELINE_TREEISH unset"
28 |
29 | log "Fetching baseline: ${BASELINE_REPO_URL}#${BASELINE_TREEISH}..."
30 | git -C "${REPO_ROOT}" fetch "${BASELINE_REPO_URL}" "${BASELINE_TREEISH}"
31 | BASELINE_COMMIT=$(git -C "${REPO_ROOT}" rev-parse FETCH_HEAD)
32 |
33 | log "Checking for API changes since ${BASELINE_REPO_URL}#${BASELINE_TREEISH} (${BASELINE_COMMIT})..."
34 | swift package --package-path "${REPO_ROOT}" diagnose-api-breaking-changes \
35 | "${BASELINE_COMMIT}" \
36 | && RC=$? || RC=$?
37 |
38 | if [ "${RC}" -ne 0 ]; then
39 | fatal "❌ Breaking API changes detected."
40 | exit "${RC}"
41 | fi
42 | log "✅ No breaking API changes detected."
43 |
--------------------------------------------------------------------------------
/Sources/Helpers/Logger/OSLogSupabaseLogger.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if canImport(OSLog)
4 | import OSLog
5 |
6 | /// A SupabaseLogger implementation that logs to OSLog.
7 | ///
8 | /// This logger maps Supabase log levels to appropriate OSLog levels:
9 | /// - `.verbose` → `.info`
10 | /// - `.debug` → `.debug`
11 | /// - `.warning` → `.notice`
12 | /// - `.error` → `.error`
13 | ///
14 | /// ## Usage
15 | ///
16 | /// ```swift
17 | /// let supabaseLogger = OSLogSupabaseLogger()
18 | ///
19 | /// // Use with Supabase client
20 | /// let supabase = SupabaseClient(
21 | /// supabaseURL: url,
22 | /// supabaseKey: key,
23 | /// options: .init(global: .init(logger: supabaseLogger))
24 | /// )
25 | /// ```
26 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
27 | public struct OSLogSupabaseLogger: SupabaseLogger {
28 | private let logger: Logger
29 |
30 | /// Creates a new OSLog-based logger with a provided Logger instance.
31 | ///
32 | /// - Parameter logger: The OSLog Logger instance to use for logging.
33 | public init(
34 | _ logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "Supabase")
35 | ) {
36 | self.logger = logger
37 | }
38 |
39 | public func log(message: SupabaseLogMessage) {
40 | let logMessage = message.description
41 |
42 | switch message.level {
43 | case .verbose:
44 | logger.info("\(logMessage, privacy: .public)")
45 | case .debug:
46 | logger.debug("\(logMessage, privacy: .public)")
47 | case .warning:
48 | logger.notice("\(logMessage, privacy: .public)")
49 | case .error:
50 | logger.error("\(logMessage, privacy: .public)")
51 | }
52 | }
53 | }
54 | #endif
55 |
--------------------------------------------------------------------------------
/Examples/Examples/Realtime/RealtimeExamplesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimeExamplesView.swift
3 | // Examples
4 | //
5 | // Demonstrates Supabase Realtime features
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RealtimeExamplesView: View {
11 | var body: some View {
12 | List {
13 | Section {
14 | Text(
15 | "Subscribe to real-time changes in your database and communicate with presence and broadcast"
16 | )
17 | .font(.caption)
18 | .foregroundColor(.secondary)
19 | }
20 |
21 | Section("Database Changes") {
22 | NavigationLink(destination: PostgresChangesView()) {
23 | ExampleRow(
24 | title: "Postgres Changes",
25 | description: "Listen to INSERT, UPDATE, DELETE events",
26 | icon: "antenna.radiowaves.left.and.right"
27 | )
28 | }
29 |
30 | NavigationLink(destination: TodoRealtimeView()) {
31 | ExampleRow(
32 | title: "Live Todo List",
33 | description: "Real-time todo updates",
34 | icon: "checklist"
35 | )
36 | }
37 | }
38 |
39 | Section("Broadcast") {
40 | NavigationLink(destination: BroadcastView()) {
41 | ExampleRow(
42 | title: "Broadcast Messages",
43 | description: "Send and receive broadcast events",
44 | icon: "megaphone"
45 | )
46 | }
47 | }
48 |
49 | Section("Presence") {
50 | NavigationLink(destination: PresenceView()) {
51 | ExampleRow(
52 | title: "Presence Tracking",
53 | description: "Track online users in real-time",
54 | icon: "person.3"
55 | )
56 | }
57 | }
58 | }
59 | .navigationTitle("Realtime")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/FunctionsTests/FunctionInvokeOptionsTests.swift:
--------------------------------------------------------------------------------
1 | import HTTPTypes
2 | import XCTest
3 |
4 | @testable import Functions
5 |
6 | final class FunctionInvokeOptionsTests: XCTestCase {
7 | func test_initWithStringBody() {
8 | let options = FunctionInvokeOptions(body: "string value")
9 | XCTAssertEqual(options.headers[.contentType], "text/plain")
10 | XCTAssertNotNil(options.body)
11 | }
12 |
13 | func test_initWithDataBody() {
14 | let options = FunctionInvokeOptions(body: "binary value".data(using: .utf8)!)
15 | XCTAssertEqual(options.headers[.contentType], "application/octet-stream")
16 | XCTAssertNotNil(options.body)
17 | }
18 |
19 | func test_initWithEncodableBody() {
20 | struct Body: Encodable {
21 | let value: String
22 | }
23 | let options = FunctionInvokeOptions(body: Body(value: "value"))
24 | XCTAssertEqual(options.headers[.contentType], "application/json")
25 | XCTAssertNotNil(options.body)
26 | }
27 |
28 | func test_initWithCustomContentType() {
29 | let boundary = "Boundary-\(UUID().uuidString)"
30 | let contentType = "multipart/form-data; boundary=\(boundary)"
31 | let options = FunctionInvokeOptions(
32 | headers: ["Content-Type": contentType],
33 | body: "binary value".data(using: .utf8)!
34 | )
35 | XCTAssertEqual(options.headers[.contentType], contentType)
36 | XCTAssertNotNil(options.body)
37 | }
38 |
39 | func testMethod() {
40 | let testCases: [FunctionInvokeOptions.Method: HTTPTypes.HTTPRequest.Method] = [
41 | .get: .get,
42 | .post: .post,
43 | .put: .put,
44 | .patch: .patch,
45 | .delete: .delete,
46 | ]
47 |
48 | for (method, expected) in testCases {
49 | XCTAssertEqual(FunctionInvokeOptions.httpMethod(method), expected)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/TestHelpers/HTTPClientMock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPClientMock.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 26/04/24.
6 | //
7 |
8 | import ConcurrencyExtras
9 | import Foundation
10 | import XCTestDynamicOverlay
11 |
12 | package actor HTTPClientMock: HTTPClientType {
13 | package struct MockNotFound: Error {}
14 |
15 | private var mocks = [@Sendable (HTTPRequest) async throws -> HTTPResponse?]()
16 |
17 | /// Requests received by this client in order.
18 | package var receivedRequests: [HTTPRequest] = []
19 |
20 | /// Responses returned by this client in order.
21 | package var returnedResponses: [Result] = []
22 |
23 | package init() {}
24 |
25 | @discardableResult
26 | package func when(
27 | _ request: @escaping @Sendable (HTTPRequest) -> Bool,
28 | return response: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse
29 | ) -> Self {
30 | mocks.append { r in
31 | if request(r) {
32 | return try await response(r)
33 | }
34 | return nil
35 | }
36 | return self
37 | }
38 |
39 | @discardableResult
40 | package func any(
41 | _ response: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse
42 | ) -> Self {
43 | when({ _ in true }, return: response)
44 | }
45 |
46 | package func send(_ request: HTTPRequest) async throws -> HTTPResponse {
47 | receivedRequests.append(request)
48 |
49 | for mock in mocks {
50 | do {
51 | if let response = try await mock(request) {
52 | returnedResponses.append(.success(response))
53 | return response
54 | }
55 | } catch {
56 | returnedResponses.append(.failure(error))
57 | throw error
58 | }
59 | }
60 |
61 | XCTFail("Mock not found for: \(request)")
62 | throw MockNotFound()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/PostgRESTTests/PostgrestResponseTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import PostgREST
4 |
5 | #if canImport(FoundationNetworking)
6 | import FoundationNetworking
7 | #endif
8 |
9 | class PostgrestResponseTests: XCTestCase {
10 | func testInit() {
11 | // Prepare data and response
12 | let data = Data()
13 | let response = HTTPURLResponse(
14 | url: URL(string: "http://example.com")!,
15 | statusCode: 200,
16 | httpVersion: nil,
17 | headerFields: ["Content-Range": "bytes 0-100/200"]
18 | )!
19 | let value = "Test Value"
20 |
21 | // Create the PostgrestResponse instance
22 | let postgrestResponse = PostgrestResponse(data: data, response: response, value: value)
23 |
24 | // Assert the properties
25 | XCTAssertEqual(postgrestResponse.data, data)
26 | XCTAssertEqual(postgrestResponse.response, response)
27 | XCTAssertEqual(postgrestResponse.value, value)
28 | XCTAssertEqual(postgrestResponse.status, 200)
29 | XCTAssertEqual(postgrestResponse.count, 200)
30 | }
31 |
32 | func testInitWithNoCount() {
33 | // Prepare data and response
34 | let data = Data()
35 | let response = HTTPURLResponse(
36 | url: URL(string: "http://example.com")!,
37 | statusCode: 200,
38 | httpVersion: nil,
39 | headerFields: ["Content-Range": "*"]
40 | )!
41 | let value = "Test Value"
42 |
43 | // Create the PostgrestResponse instance
44 | let postgrestResponse = PostgrestResponse(data: data, response: response, value: value)
45 |
46 | // Assert the properties
47 | XCTAssertEqual(postgrestResponse.data, data)
48 | XCTAssertEqual(postgrestResponse.response, response)
49 | XCTAssertEqual(postgrestResponse.value, value)
50 | XCTAssertEqual(postgrestResponse.status, 200)
51 | XCTAssertNil(postgrestResponse.count)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Examples/Examples/Storage/BucketList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BucketList.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 21/03/24.
6 | //
7 |
8 | import Supabase
9 | import SwiftUI
10 |
11 | struct BucketList: View {
12 | @State var buckets = ActionState<[Bucket], Error>.idle
13 |
14 | var body: some View {
15 | Group {
16 | switch buckets {
17 | case .idle:
18 | Color.clear
19 | case .inFlight:
20 | ProgressView()
21 | case .result(.success(let buckets)):
22 | List {
23 | ForEach(buckets, id: \.self) { bucket in
24 | NavigationLink(bucket.name, value: bucket)
25 | }
26 | }
27 | .overlay {
28 | if buckets.isEmpty {
29 | Text("No buckets found.")
30 | }
31 | }
32 | case .result(.failure(let error)):
33 | VStack {
34 | ErrorText(error)
35 | Button("Retry") {
36 | Task {
37 | await load()
38 | }
39 | }
40 | }
41 | }
42 | }
43 | .task {
44 | await load()
45 | }
46 | .navigationTitle("All buckets")
47 | .toolbar {
48 | ToolbarItem(placement: .primaryAction) {
49 | Button("Add") {
50 | Task {
51 | do {
52 | try await supabase.storage.createBucket("bucket-\(UUID().uuidString)")
53 | await load()
54 | } catch {}
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | @MainActor
62 | private func load() async {
63 | do {
64 | self.buckets = .inFlight
65 | let buckets = try await supabase.storage.listBuckets()
66 | self.buckets = .result(.success(buckets))
67 | } catch {
68 | buckets = .result(.failure(error))
69 | }
70 | }
71 | }
72 |
73 | #Preview {
74 | BucketList()
75 | }
76 |
--------------------------------------------------------------------------------
/Tests/AuthTests/PKCETests.swift:
--------------------------------------------------------------------------------
1 | import Crypto
2 | import XCTest
3 |
4 | @testable import Auth
5 |
6 | final class PKCETests: XCTestCase {
7 | let sut = PKCE.live
8 |
9 | func testGenerateCodeVerifierLength() {
10 | // The code verifier should generate a string of appropriate length
11 | // Base64 encoding of 64 random bytes should result in ~86 characters
12 | let verifier = sut.generateCodeVerifier()
13 | XCTAssertGreaterThanOrEqual(verifier.count, 85)
14 | XCTAssertLessThanOrEqual(verifier.count, 87)
15 | }
16 |
17 | func testGenerateCodeVerifierUniqueness() {
18 | // Each generated code verifier should be unique
19 | let verifier1 = sut.generateCodeVerifier()
20 | let verifier2 = sut.generateCodeVerifier()
21 | XCTAssertNotEqual(verifier1, verifier2)
22 | }
23 |
24 | func testGenerateCodeChallenge() {
25 | // Test with a known input-output pair
26 | let testVerifier = "test_verifier"
27 | let challenge = sut.generateCodeChallenge(testVerifier)
28 |
29 | // Expected value from the current implementation
30 | let expectedChallenge = "0Ku4rR8EgR1w3HyHLBCxVLtPsAAks5HOlpmTEt0XhVA"
31 | XCTAssertEqual(challenge, expectedChallenge)
32 | }
33 |
34 | func testPKCEBase64Encoding() {
35 | // Create data that will produce Base64 with special characters
36 | let testData = Data([251, 255, 191]) // This will produce Base64 with padding and special chars
37 | let encoded = testData.pkceBase64EncodedString()
38 |
39 | XCTAssertFalse(encoded.contains("+"), "Should not contain '+'")
40 | XCTAssertFalse(encoded.contains("/"), "Should not contain '/'")
41 | XCTAssertFalse(encoded.contains("="), "Should not contain '='")
42 | XCTAssertTrue(encoded.contains("-"), "Should contain '-' as replacement for '+'")
43 | XCTAssertTrue(encoded.contains("_"), "Should contain '_' as replacement for '/'")
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Examples/UserManagement/AuthView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthView.swift
3 | // UserManagement
4 | //
5 | // Created by Guilherme Souza on 17/11/23.
6 | //
7 |
8 | import Supabase
9 | import SwiftUI
10 |
11 | @MainActor
12 | struct AuthView: View {
13 | @State var email = ""
14 | @State var isLoading = false
15 | @State var result: Result?
16 |
17 | var body: some View {
18 | Form {
19 | Section {
20 | TextField("Email", text: $email)
21 | .textContentType(.emailAddress)
22 | .autocorrectionDisabled()
23 | #if os(iOS)
24 | .textInputAutocapitalization(.never)
25 | #endif
26 | }
27 |
28 | Section {
29 | Button("Sign in") {
30 | signInButtonTapped()
31 | }
32 |
33 | if isLoading {
34 | ProgressView()
35 | }
36 | }
37 |
38 | if let result {
39 | Section {
40 | switch result {
41 | case .success: Text("Check you inbox.")
42 | case .failure(let error): Text(error.localizedDescription).foregroundStyle(.red)
43 | }
44 | }
45 | }
46 | }
47 | .onMac { $0.padding() }
48 | .onOpenURL(perform: { url in
49 | Task {
50 | do {
51 | try await supabase.auth.session(from: url)
52 | } catch {
53 | result = .failure(error)
54 | }
55 | }
56 | })
57 | }
58 |
59 | func signInButtonTapped() {
60 | Task {
61 | isLoading = true
62 | defer { isLoading = false }
63 |
64 | do {
65 | try await supabase.auth.signInWithOTP(
66 | email: email,
67 | redirectTo: URL(string: "io.supabase.user-management://login-callback")
68 | )
69 | result = .success(())
70 | } catch {
71 | result = .failure(error)
72 | }
73 | }
74 | }
75 | }
76 |
77 | #Preview {
78 | AuthView()
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/RealtimeTests/RealtimePostgresFilterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealtimePostgresFilterTests.swift
3 | // Supabase
4 | //
5 | // Created by Lucas Abijmil on 20/02/2025.
6 | //
7 |
8 | import XCTest
9 | @testable import Realtime
10 |
11 | final class RealtimePostgresFilterTests: XCTestCase {
12 |
13 | func testEq() {
14 | let value = "value"
15 | let column = "column"
16 | let filter: RealtimePostgresFilter = .eq(column, value: value)
17 |
18 | XCTAssertEqual(filter.value, "column=eq.value")
19 | }
20 |
21 | func testNeq() {
22 | let value = "value"
23 | let column = "column"
24 | let filter: RealtimePostgresFilter = .neq(column, value: value)
25 |
26 | XCTAssertEqual(filter.value, "column=neq.value")
27 | }
28 |
29 | func testGt() {
30 | let value = "value"
31 | let column = "column"
32 | let filter: RealtimePostgresFilter = .gt(column, value: value)
33 |
34 | XCTAssertEqual(filter.value, "column=gt.value")
35 | }
36 |
37 | func testGte() {
38 | let value = "value"
39 | let column = "column"
40 | let filter: RealtimePostgresFilter = .gte(column, value: value)
41 |
42 | XCTAssertEqual(filter.value, "column=gte.value")
43 | }
44 |
45 | func testLt() {
46 | let value = "value"
47 | let column = "column"
48 | let filter: RealtimePostgresFilter = .lt(column, value: value)
49 |
50 | XCTAssertEqual(filter.value, "column=lt.value")
51 | }
52 |
53 | func testLte() {
54 | let value = "value"
55 | let column = "column"
56 | let filter: RealtimePostgresFilter = .lte(column, value: value)
57 |
58 | XCTAssertEqual(filter.value, "column=lte.value")
59 | }
60 |
61 | func testIn() {
62 | let values = ["value1", "value2"]
63 | let column = "column"
64 | let filter: RealtimePostgresFilter = .in(column, values: values)
65 |
66 | XCTAssertEqual(filter.value, "column=in.(value1,value2)")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Examples/Examples/Storage/FileObjectDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileObjectDetailView.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 21/03/24.
6 | //
7 |
8 | import Supabase
9 | import SwiftUI
10 |
11 | struct FileObjectDetailView: View {
12 | let api: StorageFileApi
13 | let fileObject: FileObject
14 |
15 | @Environment(\.openURL) var openURL
16 | @State var lastActionResult: (action: String, result: Any)?
17 |
18 | var body: some View {
19 | List {
20 | Section {
21 | AnyJSONView(value: try! AnyJSON(fileObject))
22 | }
23 |
24 | Section("Actions") {
25 | Button("createSignedURL") {
26 | Task {
27 | do {
28 | let url = try await api.createSignedURL(path: fileObject.name, expiresIn: 60)
29 | lastActionResult = ("createSignedURL", url)
30 | openURL(url)
31 | } catch {}
32 | }
33 | }
34 |
35 | Button("createSignedURL (download)") {
36 | Task {
37 | do {
38 | let url = try await api.createSignedURL(
39 | path: fileObject.name,
40 | expiresIn: 60,
41 | download: true
42 | )
43 | lastActionResult = ("createSignedURL (download)", url)
44 | openURL(url)
45 | } catch {}
46 | }
47 | }
48 |
49 | Button("Get info") {
50 | Task {
51 | do {
52 | let info = try await api.info(path: fileObject.name)
53 | lastActionResult = ("info", info)
54 | } catch {}
55 | }
56 | }
57 | }
58 |
59 | if let lastActionResult {
60 | Section("Last action result") {
61 | Text(lastActionResult.action)
62 | Text(stringfy(lastActionResult.result))
63 | }
64 | }
65 | }
66 | .navigationTitle(fileObject.name)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea for supabase-swift
3 | title: "[Feature]: "
4 | labels: ["enhancement", "triage"]
5 | assignees: []
6 |
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: |
11 | Thanks for suggesting a new feature! Please fill out the sections below.
12 |
13 | - type: checkboxes
14 | id: search
15 | attributes:
16 | label: Searched existing issues?
17 | description: Please search existing issues to avoid duplicates
18 | options:
19 | - label: I have searched the existing issues
20 | required: true
21 |
22 | - type: textarea
23 | id: problem
24 | attributes:
25 | label: Problem Description
26 | description: Is your feature request related to a problem? Please describe.
27 | placeholder: I'm always frustrated when...
28 | validations:
29 | required: true
30 |
31 | - type: textarea
32 | id: solution
33 | attributes:
34 | label: Proposed Solution
35 | description: Describe the solution you'd like
36 | placeholder: I would like to see...
37 | validations:
38 | required: true
39 |
40 | - type: textarea
41 | id: alternatives
42 | attributes:
43 | label: Alternative Solutions
44 | description: Describe any alternative solutions or features you've considered
45 |
46 | - type: dropdown
47 | id: priority
48 | attributes:
49 | label: Priority
50 | description: How important is this feature to you?
51 | options:
52 | - Low - Nice to have
53 | - Medium - Would significantly improve my workflow
54 | - High - Blocking my use case
55 | validations:
56 | required: true
57 |
58 | - type: textarea
59 | id: context
60 | attributes:
61 | label: Additional Context
62 | description: Add any other context, screenshots, or examples about the feature request
--------------------------------------------------------------------------------
/Sources/Helpers/JWT.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JWT.swift
3 | // Supabase
4 | //
5 | // Created by Guilherme Souza on 28/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | package struct DecodedJWT {
11 | package let header: [String: Any]
12 | package let payload: [String: Any]
13 | package let signature: Data
14 | package let raw: (header: String, payload: String)
15 | }
16 |
17 | package enum JWT {
18 | package static func decodePayload(_ jwt: String) -> [String: Any]? {
19 | let parts = jwt.split(separator: ".")
20 | guard parts.count == 3 else {
21 | return nil
22 | }
23 |
24 | let payload = String(parts[1])
25 | guard let data = Base64URL.decode(payload) else {
26 | return nil
27 | }
28 | let json = try? JSONSerialization.jsonObject(with: data, options: [])
29 | guard let decodedPayload = json as? [String: Any] else {
30 | return nil
31 | }
32 | return decodedPayload
33 | }
34 |
35 | package static func decode(_ jwt: String) -> DecodedJWT? {
36 | let parts = jwt.split(separator: ".")
37 | guard parts.count == 3 else {
38 | return nil
39 | }
40 |
41 | let headerString = String(parts[0])
42 | let payloadString = String(parts[1])
43 | let signatureString = String(parts[2])
44 |
45 | guard
46 | let headerData = Base64URL.decode(headerString),
47 | let payloadData = Base64URL.decode(payloadString),
48 | let signatureData = Base64URL.decode(signatureString),
49 | let headerJSON = try? JSONSerialization.jsonObject(with: headerData, options: []) as? [String: Any],
50 | let payloadJSON = try? JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any]
51 | else {
52 | return nil
53 | }
54 |
55 | return DecodedJWT(
56 | header: headerJSON,
57 | payload: payloadJSON,
58 | signature: signatureData,
59 | raw: (header: headerString, payload: payloadString)
60 | )
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/Helpers/HTTP/LoggerInterceptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoggerInterceptor.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 30/04/24.
6 | //
7 |
8 | import Foundation
9 |
10 | package struct LoggerInterceptor: HTTPClientInterceptor {
11 | let logger: any SupabaseLogger
12 |
13 | package init(logger: any SupabaseLogger) {
14 | self.logger = logger
15 | }
16 |
17 | package func intercept(
18 | _ request: HTTPRequest,
19 | next: @Sendable (HTTPRequest) async throws -> HTTPResponse
20 | ) async throws -> HTTPResponse {
21 | let id = UUID().uuidString
22 | return try await SupabaseLoggerTaskLocal.$additionalContext.withValue(merging: ["requestID": .string(id)]) {
23 | let urlRequest = request.urlRequest
24 |
25 | logger.verbose(
26 | """
27 | Request: \(urlRequest.httpMethod ?? "") \(urlRequest.url?.absoluteString.removingPercentEncoding ?? "")
28 | Body: \(stringfy(request.body))
29 | """
30 | )
31 |
32 | do {
33 | let response = try await next(request)
34 | logger.verbose(
35 | """
36 | Response: Status code: \(response.statusCode) Content-Length: \(
37 | response.underlyingResponse.expectedContentLength
38 | )
39 | Body: \(stringfy(response.data))
40 | """
41 | )
42 | return response
43 | } catch {
44 | logger.error("Response: Failure \(error)")
45 | throw error
46 | }
47 | }
48 | }
49 | }
50 |
51 | func stringfy(_ data: Data?) -> String {
52 | guard let data else {
53 | return ""
54 | }
55 |
56 | do {
57 | let object = try JSONSerialization.jsonObject(with: data, options: [])
58 | let prettyData = try JSONSerialization.data(
59 | withJSONObject: object,
60 | options: [.prettyPrinted, .sortedKeys]
61 | )
62 | return String(data: prettyData, encoding: .utf8) ?? ""
63 | } catch {
64 | return String(data: data, encoding: .utf8) ?? ""
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/FunctionsTests/RequestTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 23/04/24.
6 | //
7 |
8 | @testable import Functions
9 | import SnapshotTesting
10 | import XCTest
11 |
12 | final class RequestTests: XCTestCase {
13 | let url = URL(string: "http://localhost:5432/functions/v1")!
14 | let apiKey = "supabase.anon.key"
15 |
16 | func testInvokeWithDefaultOptions() async {
17 | await snapshot {
18 | try await $0.invoke("hello-world")
19 | }
20 | }
21 |
22 | func testInvokeWithCustomMethod() async {
23 | await snapshot {
24 | try await $0.invoke("hello-world", options: .init(method: .patch))
25 | }
26 | }
27 |
28 | func testInvokeWithCustomRegion() async {
29 | await snapshot {
30 | try await $0.invoke("hello-world", options: .init(region: .apNortheast1))
31 | }
32 | }
33 |
34 | func testInvokeWithCustomHeader() async {
35 | await snapshot {
36 | try await $0.invoke("hello-world", options: .init(headers: ["x-custom-key": "custom value"]))
37 | }
38 | }
39 |
40 | func testInvokeWithBody() async {
41 | await snapshot {
42 | try await $0.invoke("hello-world", options: .init(body: ["name": "Supabase"]))
43 | }
44 | }
45 |
46 | func snapshot(
47 | record: Bool = false,
48 | _ test: (FunctionsClient) async throws -> Void,
49 | file: StaticString = #file,
50 | testName: String = #function,
51 | line: UInt = #line
52 | ) async {
53 | let sut = FunctionsClient(
54 | url: url,
55 | headers: ["apikey": apiKey, "x-client-info": "functions-swift/x.y.z"]
56 | ) { request in
57 | await MainActor.run {
58 | #if os(Android)
59 | // missing snapshots for Android
60 | return
61 | #endif
62 | assertSnapshot(of: request, as: .curl, record: record, file: file, testName: testName, line: line)
63 | }
64 | throw NSError(domain: "Error", code: 0, userInfo: nil)
65 | }
66 |
67 | try? await test(sut)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/Helpers/Version.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTestDynamicOverlay
3 |
4 | private let _version = "2.38.1" // {x-release-please-version}
5 |
6 | #if DEBUG
7 | package let version = isTesting ? "0.0.0" : _version
8 | #else
9 | package let version = _version
10 | #endif
11 |
12 | private let _platform: String? = {
13 | #if os(macOS)
14 | return "macOS"
15 | #elseif os(visionOS)
16 | return "visionOS"
17 | #elseif os(iOS)
18 | #if targetEnvironment(macCatalyst)
19 | return "macCatalyst"
20 | #else
21 | if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac {
22 | return "iOSAppOnMac"
23 | }
24 | return "iOS"
25 | #endif
26 | #elseif os(watchOS)
27 | return "watchOS"
28 | #elseif os(tvOS)
29 | return "tvOS"
30 | #elseif os(Android)
31 | return "Android"
32 | #elseif os(Linux)
33 | return "Linux"
34 | #elseif os(Windows)
35 | return "Windows"
36 | #else
37 | return nil
38 | #endif
39 | }()
40 |
41 | private let _platformVersion: String? = {
42 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Windows)
43 | let majorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
44 | let minorVersion = ProcessInfo.processInfo.operatingSystemVersion.minorVersion
45 | let patchVersion = ProcessInfo.processInfo.operatingSystemVersion.patchVersion
46 | return "\(majorVersion).\(minorVersion).\(patchVersion)"
47 | #elseif os(Linux) || os(Android)
48 | if let version = try? String(contentsOfFile: "/proc/version") {
49 | return version.trimmingCharacters(in: .whitespacesAndNewlines)
50 | } else {
51 | return nil
52 | }
53 | #else
54 | nil
55 | #endif
56 | }()
57 |
58 | #if DEBUG
59 | package let platform = isTesting ? "macOS" : _platform
60 | #else
61 | package let platform = _platform
62 | #endif
63 |
64 | #if DEBUG
65 | package let platformVersion = isTesting ? "0.0.0" : _platformVersion
66 | #else
67 | package let platformVersion = _platformVersion
68 | #endif
69 |
--------------------------------------------------------------------------------
/Sources/Helpers/HTTP/HTTPFields.swift:
--------------------------------------------------------------------------------
1 | import HTTPTypes
2 |
3 | extension HTTPFields {
4 | package init(_ dictionary: [String: String]) {
5 | self.init(dictionary.map { .init(name: .init($0.key)!, value: $0.value) })
6 | }
7 |
8 | package var dictionary: [String: String] {
9 | let keyValues = self.map {
10 | ($0.name.rawName, $0.value)
11 | }
12 |
13 | return .init(keyValues, uniquingKeysWith: { $1 })
14 | }
15 |
16 | package mutating func merge(with other: Self) {
17 | for field in other {
18 | self[field.name] = field.value
19 | }
20 | }
21 |
22 | package func merging(with other: Self) -> Self {
23 | var copy = self
24 |
25 | for field in other {
26 | copy[field.name] = field.value
27 | }
28 |
29 | return copy
30 | }
31 |
32 | /// Append or update a value in header.
33 | ///
34 | /// Example:
35 | /// ```swift
36 | /// var headers: HTTPFields = [
37 | /// "Prefer": "count=exact,return=representation"
38 | /// ]
39 | ///
40 | /// headers.appendOrUpdate(.prefer, value: "return=minimal")
41 | /// #expect(headers == ["Prefer": "count=exact,return=minimal"]
42 | /// ```
43 | package mutating func appendOrUpdate(
44 | _ name: HTTPField.Name,
45 | value: String,
46 | separator: String = ","
47 | ) {
48 | if let currentValue = self[name] {
49 | var components = currentValue.components(separatedBy: separator)
50 |
51 | if let key = value.split(separator: "=").first,
52 | let index = components.firstIndex(where: { $0.hasPrefix("\(key)=") })
53 | {
54 | components[index] = value
55 | } else {
56 | components.append(value)
57 | }
58 |
59 | self[name] = components.joined(separator: separator)
60 | } else {
61 | self[name] = value
62 | }
63 | }
64 | }
65 |
66 | extension HTTPField.Name {
67 | package static let xClientInfo = HTTPField.Name("X-Client-Info")!
68 | package static let xRegion = HTTPField.Name("x-region")!
69 | package static let xRelayError = HTTPField.Name("x-relay-error")!
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/Storage/TransformOptions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Transform the asset before serving it to the client.
4 | public struct TransformOptions: Encodable, Sendable {
5 | /// The width of the image in pixels.
6 | public var width: Int?
7 | /// The height of the image in pixels.
8 | public var height: Int?
9 | /// The resize mode can be cover, contain or fill. Defaults to cover.
10 | /// Cover resizes the image to maintain it's aspect ratio while filling the entire width and height.
11 | /// Contain resizes the image to maintain it's aspect ratio while fitting the entire image within the width and height.
12 | /// Fill resizes the image to fill the entire width and height. If the object's aspect ratio does not match the width and height, the image will be stretched to fit.
13 | public var resize: String?
14 | /// Set the quality of the returned image. A number from 20 to 100, with 100 being the highest quality. Defaults to 80.
15 | public var quality: Int?
16 | /// Specify the format of the image requested.
17 | public var format: String?
18 |
19 | public init(
20 | width: Int? = nil,
21 | height: Int? = nil,
22 | resize: String? = nil,
23 | quality: Int? = 80,
24 | format: String? = nil
25 | ) {
26 | self.width = width
27 | self.height = height
28 | self.resize = resize
29 | self.quality = quality
30 | self.format = format
31 | }
32 |
33 | var queryItems: [URLQueryItem] {
34 | var items = [URLQueryItem]()
35 |
36 | if let width {
37 | items.append(URLQueryItem(name: "width", value: String(width)))
38 | }
39 |
40 | if let height {
41 | items.append(URLQueryItem(name: "height", value: String(height)))
42 | }
43 |
44 | if let resize {
45 | items.append(URLQueryItem(name: "resize", value: resize))
46 | }
47 |
48 | if let quality {
49 | items.append(URLQueryItem(name: "quality", value: String(quality)))
50 | }
51 |
52 | if let format {
53 | items.append(URLQueryItem(name: "format", value: format))
54 | }
55 |
56 | return items
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/StorageClientIntegrationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StorageClientIntegrationTests.swift
3 | //
4 | //
5 | // Created by Guilherme Souza on 07/05/24.
6 | //
7 |
8 | import InlineSnapshotTesting
9 | import Storage
10 | import XCTest
11 |
12 | final class StorageClientIntegrationTests: XCTestCase {
13 | let storage = SupabaseStorageClient(
14 | configuration: StorageClientConfiguration(
15 | url: URL(string: "\(DotEnv.SUPABASE_URL)/storage/v1")!,
16 | headers: [
17 | "Authorization": "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)",
18 | ],
19 | logger: nil
20 | )
21 | )
22 |
23 | func testBucket_CRUD() async throws {
24 | let bucketName = "test-bucket"
25 |
26 | var buckets = try await storage.listBuckets()
27 | XCTAssertFalse(buckets.contains(where: { $0.name == bucketName }))
28 |
29 | try await storage.createBucket(bucketName, options: .init(public: true))
30 |
31 | var bucket = try await storage.getBucket(bucketName)
32 | XCTAssertEqual(bucket.name, bucketName)
33 | XCTAssertEqual(bucket.id, bucketName)
34 | XCTAssertEqual(bucket.isPublic, true)
35 |
36 | buckets = try await storage.listBuckets()
37 | XCTAssertTrue(buckets.contains { $0.id == bucket.id })
38 |
39 | try await storage.updateBucket(bucketName, options: BucketOptions(allowedMimeTypes: ["image/jpeg"]))
40 |
41 | bucket = try await storage.getBucket(bucketName)
42 | XCTAssertEqual(bucket.allowedMimeTypes, ["image/jpeg"])
43 |
44 | try await storage.deleteBucket(bucketName)
45 |
46 | buckets = try await storage.listBuckets()
47 | XCTAssertFalse(buckets.contains { $0.id == bucket.id })
48 | }
49 |
50 | func testGetBucketWithWrongId() async {
51 | do {
52 | _ = try await storage.getBucket("not-exist-id")
53 | XCTFail("Unexpected success")
54 | } catch {
55 | assertInlineSnapshot(of: error, as: .dump) {
56 | """
57 | ▿ StorageError
58 | ▿ error: Optional
59 | - some: "Bucket not found"
60 | - message: "Bucket not found"
61 | ▿ statusCode: Optional
62 | - some: "404"
63 |
64 | """
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Examples/Examples/Auth/SignInWithFacebook.swift:
--------------------------------------------------------------------------------
1 | import FacebookLogin
2 | import OSLog
3 | import Supabase
4 | import SwiftUI
5 |
6 | struct SignInWithFacebook: View {
7 | @State private var actionState = ActionState.idle
8 |
9 | static let logger = Logger(subsystem: "com.supabase.examples", category: "SignInWithFacebook")
10 |
11 | let loginManager = LoginManager()
12 |
13 | var body: some View {
14 | VStack {
15 | Button("Sign in with Facebook") {
16 | actionState = .inFlight
17 |
18 | loginManager.logIn(
19 | configuration: LoginConfiguration(
20 | permissions: ["public_profile", "email"],
21 | tracking: .limited
22 | )
23 | ) { result in
24 | switch result {
25 | case .failed(let error):
26 | actionState = .result(.failure(error))
27 | Self.logger.error("Facebook login failed: \(error.localizedDescription)")
28 | case .cancelled:
29 | actionState = .idle
30 | Self.logger.info("Facebook login cancelled")
31 | case .success(_, _, let token):
32 | Self.logger.info("Facebook login succeeded.")
33 |
34 | guard let idToken = token?.tokenString else {
35 | actionState = .idle
36 | Self.logger.error("Facebook login token is nil")
37 | return
38 | }
39 |
40 | Task {
41 | do {
42 | try await supabase.auth.signInWithIdToken(
43 | credentials: OpenIDConnectCredentials(
44 | provider: .facebook,
45 | idToken: idToken
46 | )
47 | )
48 | actionState = .result(.success(()))
49 | Self.logger.info("Successfully signed in with Facebook")
50 | } catch {
51 | actionState = .result(.failure(error))
52 | Self.logger.error("Failed to sign in with Facebook: \(error.localizedDescription)")
53 |
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 | .gitHubSourceLink()
61 | }
62 | }
63 |
64 | #Preview {
65 | SignInWithFacebook()
66 | }
67 |
--------------------------------------------------------------------------------
/Examples/Examples/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // Examples
4 | //
5 | // Created by Guilherme Souza on 23/12/22.
6 | //
7 |
8 | import Supabase
9 | import SwiftUI
10 |
11 | struct HomeView: View {
12 | @Environment(AuthController.self) var auth
13 |
14 | var body: some View {
15 | @Bindable var auth = auth
16 |
17 | TabView {
18 | // Database Tab
19 | NavigationStack {
20 | DatabaseExamplesView()
21 | }
22 | .tabItem {
23 | Label("Database", systemImage: "cylinder.split.1x2")
24 | }
25 |
26 | // Realtime Tab
27 | NavigationStack {
28 | RealtimeExamplesView()
29 | }
30 | .tabItem {
31 | Label("Realtime", systemImage: "bolt")
32 | }
33 |
34 | // Storage Tab
35 | NavigationStack {
36 | StorageExamplesView()
37 | .navigationDestination(for: Bucket.self, destination: BucketDetailView.init)
38 | }
39 | .tabItem {
40 | Label("Storage", systemImage: "externaldrive")
41 | }
42 |
43 | // Functions Tab
44 | NavigationStack {
45 | FunctionsExamplesView()
46 | }
47 | .tabItem {
48 | Label("Functions", systemImage: "function")
49 | }
50 |
51 | // Profile Tab
52 | ProfileView()
53 | .tabItem {
54 | Label("Profile", systemImage: "person.circle")
55 | }
56 | }
57 | .sheet(isPresented: $auth.isPasswordRecoveryFlow) {
58 | UpdatePasswordView()
59 | }
60 | }
61 |
62 | struct UpdatePasswordView: View {
63 | @Environment(\.dismiss) var dismiss
64 |
65 | @State var password: String = ""
66 |
67 | var body: some View {
68 | Form {
69 | SecureField("Password", text: $password)
70 | .textContentType(.newPassword)
71 |
72 | Button("Update password") {
73 | Task {
74 | do {
75 | try await supabase.auth.update(user: UserAttributes(password: password))
76 | dismiss()
77 | } catch {}
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | struct HomeView_Previews: PreviewProvider {
86 | static var previews: some View {
87 | HomeView()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Tests/StorageTests/TransformOptionsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Storage
4 |
5 | final class TransformOptionsTests: XCTestCase {
6 | func testDefaultInitialization() {
7 | let options = TransformOptions()
8 |
9 | XCTAssertNil(options.width)
10 | XCTAssertNil(options.height)
11 | XCTAssertNil(options.resize)
12 | XCTAssertEqual(options.quality, 80) // Default value
13 | XCTAssertNil(options.format)
14 | }
15 |
16 | func testCustomInitialization() {
17 | let options = TransformOptions(
18 | width: 100,
19 | height: 200,
20 | resize: "cover",
21 | quality: 90,
22 | format: "webp"
23 | )
24 |
25 | XCTAssertEqual(options.width, 100)
26 | XCTAssertEqual(options.height, 200)
27 | XCTAssertEqual(options.resize, "cover")
28 | XCTAssertEqual(options.quality, 90)
29 | XCTAssertEqual(options.format, "webp")
30 | }
31 |
32 | func testQueryItemsGeneration() {
33 | let options = TransformOptions(
34 | width: 100,
35 | height: 200,
36 | resize: "cover",
37 | quality: 90,
38 | format: "webp"
39 | )
40 |
41 | let queryItems = options.queryItems
42 |
43 | XCTAssertEqual(queryItems.count, 5)
44 |
45 | XCTAssertEqual(queryItems[0].name, "width")
46 | XCTAssertEqual(queryItems[0].value, "100")
47 |
48 | XCTAssertEqual(queryItems[1].name, "height")
49 | XCTAssertEqual(queryItems[1].value, "200")
50 |
51 | XCTAssertEqual(queryItems[2].name, "resize")
52 | XCTAssertEqual(queryItems[2].value, "cover")
53 |
54 | XCTAssertEqual(queryItems[3].name, "quality")
55 | XCTAssertEqual(queryItems[3].value, "90")
56 |
57 | XCTAssertEqual(queryItems[4].name, "format")
58 | XCTAssertEqual(queryItems[4].value, "webp")
59 | }
60 |
61 | func testPartialQueryItemsGeneration() {
62 | let options = TransformOptions(width: 100, quality: 75)
63 |
64 | let queryItems = options.queryItems
65 |
66 | XCTAssertEqual(queryItems.count, 2)
67 |
68 | XCTAssertEqual(queryItems[0].name, "width")
69 | XCTAssertEqual(queryItems[0].value, "100")
70 |
71 | XCTAssertEqual(queryItems[1].name, "quality")
72 | XCTAssertEqual(queryItems[1].value, "75")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report to help us improve
3 | title: "[Bug]: "
4 | labels: ["bug", "triage"]
5 | assignees: []
6 |
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: |
11 | Thanks for taking the time to fill out this bug report!
12 |
13 | - type: input
14 | id: version
15 | attributes:
16 | label: Version
17 | description: What version of supabase-swift are you running?
18 | placeholder: ex. 2.31.2
19 | validations:
20 | required: true
21 |
22 | - type: dropdown
23 | id: platform
24 | attributes:
25 | label: Platform
26 | description: What platform are you using?
27 | options:
28 | - iOS
29 | - macOS
30 | - tvOS
31 | - watchOS
32 | - visionOS
33 | - Linux
34 | - Other
35 | validations:
36 | required: true
37 |
38 | - type: input
39 | id: swift-version
40 | attributes:
41 | label: Swift Version
42 | description: What version of Swift are you using?
43 | placeholder: ex. 5.10
44 | validations:
45 | required: true
46 |
47 | - type: textarea
48 | id: what-happened
49 | attributes:
50 | label: What happened?
51 | description: Also tell us, what did you expect to happen?
52 | placeholder: Tell us what you see!
53 | validations:
54 | required: true
55 |
56 | - type: textarea
57 | id: reproduce
58 | attributes:
59 | label: Steps to Reproduce
60 | description: Please provide clear steps to reproduce the issue
61 | placeholder: |
62 | 1. Import Supabase
63 | 2. Create client with '...'
64 | 3. Call method '...'
65 | 4. See error
66 | validations:
67 | required: true
68 |
69 | - type: textarea
70 | id: code-sample
71 | attributes:
72 | label: Code Sample
73 | description: Please provide a minimal code sample that reproduces the issue
74 | render: swift
75 |
76 | - type: textarea
77 | id: logs
78 | attributes:
79 | label: Relevant log output
80 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
81 | render: shell
--------------------------------------------------------------------------------