Convert SatsPrice to a Skip multiplatform app

This commit is contained in:
2024-08-31 10:24:07 +03:00
parent 29650a3ea4
commit 7f22956557
101 changed files with 1379 additions and 1095 deletions

104
.gitignore vendored Normal file
View File

@@ -0,0 +1,104 @@
## User settings
xcuserdata/
xcodebuild*.log
.gradle/
.idea/
.*.swp
.*.swo
.DS_Store
Android/app/keystore.jks
Android/app/keystore.properties
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
.android/
.kotlin/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
Packages/
Package.pins
Package.resolved
#*.xcodeproj
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
**/fastlane/apikey.json
**/fastlane/report.xml
**/fastlane/README.md
**/fastlane/Preview.html
**/fastlane/screenshots/**/*.png
**/fastlane/metadata/android/*/images/**/*.png
**/fastlane/test_output
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# gradle properties
local.properties

View File

@@ -0,0 +1,64 @@
import java.util.Properties
plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.android.application)
id("skip-build-plugin")
}
skip {
}
android {
namespace = group as String
compileSdk = libs.versions.android.sdk.compile.get().toInt()
compileOptions {
sourceCompatibility = JavaVersion.toVersion(libs.versions.jvm.get())
targetCompatibility = JavaVersion.toVersion(libs.versions.jvm.get())
}
kotlinOptions {
jvmTarget = libs.versions.jvm.get().toString()
}
packaging {
jniLibs.keepDebugSymbols.add("**/*.so")
}
defaultConfig {
minSdk = libs.versions.android.sdk.min.get().toInt()
targetSdk = libs.versions.android.sdk.compile.get().toInt()
// skip.tools.skip-build-plugin will automatically use Skip.env properties for:
// applicationId = PRODUCT_BUNDLE_IDENTIFIER
// versionCode = CURRENT_PROJECT_VERSION
// versionName = MARKETING_VERSION
}
buildFeatures {
buildConfig = true
}
// default signing configuration tries to load from keystore.properties
signingConfigs {
val keystorePropertiesFile = file("keystore.properties")
if (keystorePropertiesFile.isFile) {
create("release") {
val keystoreProperties = Properties()
keystoreProperties.load(keystorePropertiesFile.inputStream())
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
}
buildTypes {
release {
signingConfig = signingConfigs.findByName("release")
isMinifyEnabled = true
isShrinkResources = true
isDebuggable = false // can be set to true for debugging release build, but needs to be false when uploading to store
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
}

4
Android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,4 @@
-keeppackagenames **
-keep class skip.** { *; }
-keep class com.sun.jna.Pointer { *; }
-keep class sats.price.** { *; }

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This AndroidManifest.xml template was generated by Skip -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- example permissions for using device location -->
<!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> -->
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> -->
<!-- permissions needed for using the internet or an embedded WebKit browser -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
<application
android:label="${PRODUCT_NAME}"
android:name=".AndroidAppMain"
android:supportsRtl="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,125 @@
package sats.price
import skip.lib.*
import skip.model.*
import skip.foundation.*
import skip.ui.*
import android.Manifest
import android.app.Application
import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
internal val logger: SkipLogger = SkipLogger(subsystem = "sats.price", category = "SatsPrice")
/// AndroidAppMain is the `android.app.Application` entry point, and must match `application android:name` in the AndroidMainfest.xml file.
open class AndroidAppMain: Application {
constructor() {
}
override fun onCreate() {
super.onCreate()
logger.info("starting app")
ProcessInfo.launch(applicationContext)
}
companion object {
}
}
/// AndroidAppMain is initial `androidx.appcompat.app.AppCompatActivity`, and must match `activity android:name` in the AndroidMainfest.xml file.
open class MainActivity: AppCompatActivity {
constructor() {
}
override fun onCreate(savedInstanceState: android.os.Bundle?) {
super.onCreate(savedInstanceState)
logger.info("starting activity")
UIApplication.launch(this)
enableEdgeToEdge()
setContent {
val saveableStateHolder = rememberSaveableStateHolder()
saveableStateHolder.SaveableStateProvider(true) {
PresentationRootView(ComposeContext())
}
}
// Example of requesting permissions on startup.
// These must match the permissions in the AndroidManifest.xml file.
//let permissions = listOf(
// Manifest.permission.ACCESS_COARSE_LOCATION,
// Manifest.permission.ACCESS_FINE_LOCATION
// Manifest.permission.CAMERA,
// Manifest.permission.WRITE_EXTERNAL_STORAGE,
//)
//let requestTag = 1
//ActivityCompat.requestPermissions(self, permissions.toTypedArray(), requestTag)
}
override fun onSaveInstanceState(bundle: android.os.Bundle): Unit = super.onSaveInstanceState(bundle)
override fun onRestoreInstanceState(bundle: android.os.Bundle) {
// Usually you restore your state in onCreate(). It is possible to restore it in onRestoreInstanceState() as well, but not very common. (onRestoreInstanceState() is called after onStart(), whereas onCreate() is called before onStart().
logger.info("onRestoreInstanceState")
super.onRestoreInstanceState(bundle)
}
override fun onRestart() {
logger.info("onRestart")
super.onRestart()
}
override fun onStart() {
logger.info("onStart")
super.onStart()
}
override fun onResume() {
logger.info("onResume")
super.onResume()
}
override fun onPause() {
logger.info("onPause")
super.onPause()
}
override fun onStop() {
logger.info("onStop")
super.onStop()
}
override fun onDestroy() {
logger.info("onDestroy")
super.onDestroy()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: kotlin.Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
logger.info("onRequestPermissionsResult: ${requestCode}")
}
companion object {
}
}
@Composable
internal fun PresentationRootView(context: ComposeContext) {
val colorScheme = if (isSystemInDarkTheme()) ColorScheme.dark else ColorScheme.light
PresentationRoot(defaultColorScheme = colorScheme, context = context) { ctx ->
val contentContext = ctx.content()
Box(modifier = ctx.modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
RootView().Compose(context = contentContext)
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

11
Android/fastlane/Appfile Normal file
View File

@@ -0,0 +1,11 @@
# This file contains the app distribution configuration
# for the Android half of the Skip app.
# You can find the documentation at https://docs.fastlane.tools
# Load the shared Skip.env properties with the app info
require('dotenv')
Dotenv.load '../../Skip.env'
package_name(ENV['PRODUCT_BUNDLE_IDENTIFIER'])
# Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
json_key_file("fastlane/apikey.json")

49
Android/fastlane/Fastfile Normal file
View File

@@ -0,0 +1,49 @@
# This file contains the fastlane.tools configuration
# for the Android half of the Skip app.
# You can find the documentation at https://docs.fastlane.tools
# Load the shared Skip.env properties with the app info
require('dotenv')
Dotenv.load '../../Skip.env'
default_platform(:android)
# use the Homebrew gradle rather than expecting a local gradlew
gradle_bin = (ENV['HOMEBREW_PREFIX'] ? ENV['HOMEBREW_PREFIX'] : "/opt/homebrew") + "/bin/gradle"
default_platform(:android)
desc "Build Skip Android App"
lane :build do |options|
build_config = (options[:release] ? "Release" : "Debug")
gradle(
task: "build${build_config}",
gradle_path: gradle_bin,
flags: "--warning-mode none -x lint"
)
end
desc "Test Skip Android App"
lane :test do
gradle(
task: "test",
gradle_path: gradle_bin
)
end
desc "Assemble Skip Android App"
lane :assemble do
gradle(
gradle_path: gradle_bin,
task: "bundleRelease"
)
# sh "your_script.sh"
end
desc "Deploy Skip Android App to Google Play"
lane :release do
assemble
upload_to_play_store(
aab: '../.build/Android/app/outputs/bundle/release/app-release.aab'
)
end

View File

@@ -0,0 +1 @@
A great new app built with Skip!

View File

@@ -0,0 +1 @@
A great new app built with Skip!

View File

@@ -0,0 +1 @@
SatsPrice

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4g
android.useAndroidX=true
kotlin.code.style=official

View File

@@ -0,0 +1,2 @@
#Sat Aug 31 09:41:39 EEST 2024
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip

View File

@@ -0,0 +1,46 @@
// This gradle project is part of a conventional Skip app project.
// It invokes the shared build skip plugin logic, which included as part of the skip-unit buildSrc
// When built from Android Studio, it uses the BUILT_PRODUCTS_DIR folder to share the same build outputs as Xcode, otherwise it uses SwiftPM's .build/ folder
pluginManagement {
// local override of BUILT_PRODUCTS_DIR
if (System.getenv("BUILT_PRODUCTS_DIR") == null) {
//System.setProperty("BUILT_PRODUCTS_DIR", "${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData/MySkipProject-aqywrhrzhkbvfseiqgxuufbdwdft/Build/Products/Debug-iphonesimulator")
}
// the source for the plugin is linked as part of the SkipUnit transpilation
val skipOutput = System.getenv("BUILT_PRODUCTS_DIR") ?: System.getProperty("BUILT_PRODUCTS_DIR")
val outputExt = if (skipOutput != null) ".output" else "" // Xcode saves output in package-name.output; SPM has no suffix
val skipOutputs: File = if (skipOutput != null) {
// BUILT_PRODUCTS_DIR is set when building from Xcode, in which case we will use Xcode's DerivedData plugin output
file(skipOutput).resolve("../../../SourcePackages/plugins/")
} else {
exec {
// create transpiled Kotlin and generate Gradle projects from SwiftPM modules
commandLine("swift", "build")
workingDir = file("..")
}
// SPM output folder is a peer of the parent Package.swift
rootDir.resolve("../.build/plugins/outputs/")
}
// load the Skip plugin (part of the skip-unit project), which handles configuring the Android project
// because this path is a symlink, we need to use the canonical path or gradle will mis-interpret it as a different build source
var pluginSource = skipOutputs.resolve("skip-unit${outputExt}/SkipUnit/skipstone/buildSrc/").canonicalFile
if (!pluginSource.isDirectory) {
// check new SwiftPM6 plugin "destination" folder for command-line builds
pluginSource = skipOutputs.resolve("skip-unit${outputExt}/SkipUnit/destination/skipstone/buildSrc/").canonicalFile
}
if (!pluginSource.isDirectory) {
throw GradleException("Missing expected Skip output folder: ${pluginSource}. Run `swift build` in the root folder to create, or specify Xcode environment BUILT_PRODUCTS_DIR.")
}
includeBuild(pluginSource.path) {
name = "skip-plugins"
}
}
plugins {
id("skip-plugin") apply true
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
</dict>
</plist>

View File

@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>SatsPrice.xcdatamodel</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

56
Darwin/SatsPrice.xcconfig Normal file
View File

@@ -0,0 +1,56 @@
#include "../Skip.env"
// Set the action that will be executed as part of the Xcode Run Script phase
// Setting to "launch" will build and run the app in the first open Android emulator or device
// Setting to "build" will just run gradle build, but will not launch the app
SKIP_ACTION = launch
//SKIP_ACTION = build
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
INFOPLIST_FILE = Info.plist
GENERATE_INFOPLIST_FILE = YES
// The user-visible name of the app (localizable)
//INFOPLIST_KEY_CFBundleDisplayName = App Name
//INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.utilities
// iOS-specific Info.plist property keys
INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*] = YES
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*] = YES
INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*] = YES
INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault
INFOPLIST_KEY_UISupportedInterfaceOrientations[sdk=iphone*] = UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown
IPHONEOS_DEPLOYMENT_TARGET = 16.0
MACOSX_DEPLOYMENT_TARGET = 13.0
SUPPORTS_MACCATALYST = NO
// iPhone + iPad
TARGETED_DEVICE_FAMILY = 1,2
// iPhone only
// TARGETED_DEVICE_FAMILY = 1
SWIFT_EMIT_LOC_STRINGS = YES
// the name of the product module; this can be anything, but cannot conflict with any Swift module names
PRODUCT_MODULE_NAME = $(PRODUCT_NAME:c99extidentifier)App
// On-device testing may need to override the bundle ID
// PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=iphoneos*] = cool.beans.BundleIdentifer
SDKROOT = auto
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
SWIFT_EMIT_LOC_STRINGS = YES
SWIFT_VERSION = 5.0
//SWIFT_VERSION = 6.0
// Development team ID for on-device testing
CODE_SIGNING_REQUIRED = NO
CODE_SIGN_STYLE = Automatic
CODE_SIGN_ENTITLEMENTS = Entitlements.plist
//CODE_SIGNING_IDENTITY = -
//DEVELOPMENT_TEAM =

View File

@@ -0,0 +1,294 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
49231BAC2AC5BCEF00F98ADF /* SatsPriceApp in Frameworks */ = {isa = PBXBuildFile; productRef = 49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */; };
49231BAD2AC5BCEF00F98ADF /* SatsPriceApp in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */; };
499CD43B2AC5B799001AE8D8 /* SatsPriceAppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F90C2B2A52156200F06D93 /* SatsPriceAppMain.swift */; };
499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49F90C2F2A52156300F06D93 /* Assets.xcassets */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
49231BAD2AC5BCEF00F98ADF /* SatsPriceApp in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4900101C2BACEA710000DE33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
493609562A6B7EAE00C401E2 /* SatsPrice */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SatsPrice; path = ..; sourceTree = "<group>"; };
496BDBEB2B89A47800C09264 /* SatsPrice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SatsPrice.app; sourceTree = BUILT_PRODUCTS_DIR; };
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = ../Sources/SatsPrice/Resources/Localizable.xcstrings; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SatsPrice.xcconfig; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
499AB9082B0581F4005E8330 /* plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = plugins; path = ../../../SourcePackages/plugins; sourceTree = BUILT_PRODUCTS_DIR; };
49F90C2B2A52156200F06D93 /* SatsPriceAppMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SatsPriceAppMain.swift; path = Sources/SatsPriceAppMain.swift; sourceTree = SOURCE_ROOT; };
49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
499CD43C2AC5B799001AE8D8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
49231BAC2AC5BCEF00F98ADF /* SatsPriceApp in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
496BDBEC2B89A47800C09264 /* Products */ = {
isa = PBXGroup;
children = (
496BDBEB2B89A47800C09264 /* SatsPrice.app */,
);
name = Products;
sourceTree = "<group>";
};
49AB54462B066A7E007B79B2 /* SkipStone */ = {
isa = PBXGroup;
children = (
499AB9082B0581F4005E8330 /* plugins */,
);
name = SkipStone;
sourceTree = "<group>";
};
49F90C1F2A52156200F06D93 = {
isa = PBXGroup;
children = (
496EB72F2A6AE4DE00C1253C /* README.md */,
496EB72F2A6AE4DE00C1253A /* Skip.env */,
496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */,
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
493609562A6B7EAE00C401E2 /* SatsPrice */,
49F90C2A2A52156200F06D93 /* App */,
49AB54462B066A7E007B79B2 /* SkipStone */,
496BDBEC2B89A47800C09264 /* Products */,
);
sourceTree = "<group>";
};
49F90C2A2A52156200F06D93 /* App */ = {
isa = PBXGroup;
children = (
49F90C2B2A52156200F06D93 /* SatsPriceAppMain.swift */,
49F90C2F2A52156300F06D93 /* Assets.xcassets */,
49F90C312A52156300F06D93 /* Entitlements.plist */,
4900101C2BACEA710000DE33 /* Info.plist */,
);
name = App;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
499CD4382AC5B799001AE8D8 /* SatsPrice */ = {
isa = PBXNativeTarget;
buildConfigurationList = 499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "SatsPrice" */;
buildPhases = (
499CD43A2AC5B799001AE8D8 /* Sources */,
499CD43C2AC5B799001AE8D8 /* Frameworks */,
499CD43E2AC5B799001AE8D8 /* Resources */,
499CD4452AC5B869001AE8D8 /* Run skip gradle */,
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = SatsPrice;
packageProductDependencies = (
49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */,
);
productName = App;
productReference = 496BDBEB2B89A47800C09264 /* SatsPrice.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
49F90C202A52156200F06D93 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1540;
};
buildConfigurationList = 49F90C232A52156200F06D93 /* Build configuration list for PBXProject "SatsPrice" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 49F90C1F2A52156200F06D93;
packageReferences = (
);
productRefGroup = 496BDBEC2B89A47800C09264 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
499CD4382AC5B799001AE8D8 /* SatsPrice */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
499CD43E2AC5B799001AE8D8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */,
496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
499CD4452AC5B869001AE8D8 /* Run skip gradle */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run skip gradle";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = "/bin/sh -e";
shellScript = "if [ \"${SKIP_ZERO}\" != \"\" ]; then\n\techo \"note: skipping skip due to SKIP_ZERO\"\n\texit 0\nelif [ \"${ENABLE_PREVIEWS}\" == \"YES\" ]; then\n\techo \"note: skipping skip due to ENABLE_PREVIEWS\"\n\texit 0\nelif [ \"${ACTION}\" == \"install\" ]; then\n\techo \"note: skipping skip due to archive install\"\n\texit 0\nelse\n\tSKIP_ACTION=\"${SKIP_ACTION:-launch}\"\nfi\nPATH=${BUILD_ROOT}/Debug:${BUILD_ROOT}/../../SourcePackages/artifacts/skip/skip/skip.artifactbundle/macos:${PATH}:${HOMEBREW_PREFIX:-/opt/homebrew}/bin\necho \"note: running gradle build with: $(which skip) gradle -p ${PWD}/../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\"\nskip gradle -p ../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
499CD43A2AC5B799001AE8D8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
499CD43B2AC5B799001AE8D8 /* SatsPriceAppMain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
499CD4422AC5B799001AE8D8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = S99A5B637C;
ENABLE_PREVIEWS = YES;
INFOPLIST_KEY_CFBundleDisplayName = SatsPrice;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MARKETING_VERSION = 0.2.0;
};
name = Debug;
};
499CD4432AC5B799001AE8D8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = S99A5B637C;
ENABLE_PREVIEWS = YES;
INFOPLIST_KEY_CFBundleDisplayName = SatsPrice;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MARKETING_VERSION = 0.2.0;
};
name = Release;
};
49F90C4B2A52156300F06D93 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
49F90C4C2A52156300F06D93 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "SatsPrice" */ = {
isa = XCConfigurationList;
buildConfigurations = (
499CD4422AC5B799001AE8D8 /* Debug */,
499CD4432AC5B799001AE8D8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
49F90C232A52156200F06D93 /* Build configuration list for PBXProject "SatsPrice" */ = {
isa = XCConfigurationList;
buildConfigurations = (
49F90C4B2A52156300F06D93 /* Debug */,
49F90C4C2A52156300F06D93 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */ = {
isa = XCSwiftPackageProductDependency;
productName = SatsPriceApp;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 49F90C202A52156200F06D93 /* Project object */;
}

View File

@@ -0,0 +1,10 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
import SwiftUI
import SatsPrice
/// The entry point to the app simply loads the App implementation from SPM module.
@main struct AppMain: App, SatsPriceApp {
}

View File

@@ -0,0 +1,5 @@
// Additional properties included by the Fastfile build_app
// This file can be used to override various properties from Skip.env
//PRODUCT_BUNDLE_IDENTIFIER =
//DEVELOPMENT_TEAM =

8
Darwin/fastlane/Appfile Normal file
View File

@@ -0,0 +1,8 @@
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
require('dotenv')
Dotenv.load '../../Skip.env'
#app_identifier(ENV['PRODUCT_BUNDLE_IDENTIFIER'])
# apple_id("my@email")

View File

@@ -0,0 +1,28 @@
copyright "#{Time.now.year}"
default_language("en-US")
force(true) # Skip HTML report verification
automatic_release(true)
skip_screenshots(false)
precheck_include_in_app_purchases(false)
#skip_binary_upload(true)
submit_for_review(true)
submission_information({
add_id_info_serves_ads: false,
add_id_info_uses_idfa: false,
add_id_info_tracks_install: false,
add_id_info_tracks_action: false,
add_id_info_limits_tracking: false,
content_rights_has_rights: false,
content_rights_contains_third_party_content: false,
export_compliance_contains_third_party_cryptography: false,
export_compliance_encryption_updated: false,
export_compliance_platform: 'ios',
export_compliance_compliance_required: false,
export_compliance_uses_encryption: false,
export_compliance_is_exempt: false,
export_compliance_contains_proprietary_cryptography: false
})

32
Darwin/fastlane/Fastfile Normal file
View File

@@ -0,0 +1,32 @@
# This file contains the fastlane.tools configuration
# for the iOS half of the Skip app.
# You can find the documentation at https://docs.fastlane.tools
default_platform(:ios)
lane :assemble do |options|
# only build the iOS side of the app
ENV["SKIP_ZERO"] = "true"
build_app(
sdk: "iphoneos",
xcconfig: "fastlane/AppStore.xcconfig",
xcargs: "-skipPackagePluginValidation -skipMacroValidation",
derived_data_path: "../.build/Darwin/DerivedData",
output_directory: "../.build/fastlane/Darwin",
skip_archive: ENV["FASTLANE_SKIP_ARCHIVE"] == "YES",
skip_codesigning: ENV["FASTLANE_SKIP_CODESIGNING"] == "YES"
)
end
lane :release do |options|
desc "Build and release app"
assemble
upload_to_app_store(
api_key_path: "fastlane/apikey.json",
app_rating_config_path: "fastlane/metadata/rating.json",
release_notes: { default: "Fixes and improvements." }
)
end

View File

@@ -0,0 +1 @@
A great new app built with Skip!

View File

@@ -0,0 +1 @@
app,key,words

View File

@@ -0,0 +1 @@
https://example.org/privacy/

View File

@@ -0,0 +1 @@
Bug fixes and performance improvements.

View File

@@ -0,0 +1 @@
https://example.org/app/

View File

@@ -0,0 +1 @@
A new Skip app

View File

@@ -0,0 +1 @@
https://example.org/support/

View File

@@ -0,0 +1 @@
SatsPrice

View File

@@ -0,0 +1 @@
New features and better performance.

View File

@@ -0,0 +1,17 @@
{
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingSimulated": "NONE",
"horrorOrFearThemes": "NONE",
"matureOrSuggestiveThemes": "NONE",
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"violenceCartoonOrFantasy": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"violenceRealistic": "NONE",
"gambling": false,
"seventeenPlus": false,
"unrestrictedWebAccess": false
}

24
Package.swift Normal file
View File

@@ -0,0 +1,24 @@
// swift-tools-version: 5.9
// This is a Skip (https://skip.tools) package,
// containing a Swift Package Manager project
// that will use the Skip build plugin to transpile the
// Swift Package, Sources, and Tests into an
// Android Gradle Project with Kotlin sources and JUnit tests.
import PackageDescription
let package = Package(
name: "sats-price",
defaultLocalization: "en",
platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
products: [
.library(name: "SatsPriceApp", type: .dynamic, targets: ["SatsPrice"]),
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.0.7"),
.package(url: "https://source.skip.tools/skip-ui.git", from: "1.0.0")
],
targets: [
.target(name: "SatsPrice", dependencies: [.product(name: "SkipUI", package: "skip-ui")], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.testTarget(name: "SatsPriceTests", dependencies: ["SatsPrice", .product(name: "SkipTest", package: "skip")], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
]
)

View File

@@ -2,14 +2,49 @@
This app fetches the price of Bitcoin relative to the United States Dollar from multiple sources, and converts inputted amounts between Sats, BTC, and USD.
## Notes
This is a free [Skip](https://skip.tools) dual-platform app project.
It builds a native app for both iOS and Android.
Precision is attempted to be kept up to 20 digits.
## Building
When NaN is displayed, it means "not a number", which can be caused by invalid input or problems fetching the price of Bitcoin from the selected source.
This project is both a stand-alone Swift Package Manager module,
as well as an Xcode project that builds and transpiles the project
into a Kotlin Gradle project for Android using the Skip plugin.
Building the module requires that Skip be installed using
[Homebrew](https://brew.sh) with `brew install skiptools/skip/skip`.
This will also install the necessary transpiler prerequisites:
Kotlin, Gradle, and the Android build tools.
Installation prerequisites can be confirmed by running `skip checkup`.
## Testing
The module can be tested using the standard `swift test` command
or by running the test target for the macOS destination in Xcode,
which will run the Swift tests as well as the transpiled
Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with `skip test`,
which will output a table of the test results for both platforms.
## Running
Xcode and Android Studio must be downloaded and installed in order to
run the app in the iOS simulator / Android emulator.
An Android emulator must already be running, which can be launched from
Android Studio's Device Manager.
To run both the Swift and Kotlin apps simultaneously,
launch the SatsPriceApp target from Xcode.
A build phases runs the "Launch Android APK" script that
will deploy the transpiled app a running Android emulator or connected device.
Logging output for the iOS app can be viewed in the Xcode console, and in
Android Studio's logcat tab for the transpiled Kotlin app.
## Attribution
The [Bitcoin Calculator](https://www.flaticon.com/free-icons/bitcoin-calculator) icon was created by Icon home and licensed as free for personal and commercial use with attribution.
This project depends on [Skip](https://skip.tools) to build as a multi-platform app.
This project depends on [BigDecimal](https://github.com/mgriebling/BigDecimal), an MIT-licensed package for providing arbitrary-precision and fixed-precision decimal arithmetic in Swift.
The [Bitcoin Calculator](https://www.flaticon.com/free-icons/bitcoin-calculator) icon was created by Icon home and licensed as free for personal and commercial use with attribution.

View File

@@ -1,718 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
3A7B2AA32B86407A00ACC4A7 /* FakePriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7B2AA22B86407A00ACC4A7 /* FakePriceFetcher.swift */; };
3A8E5DA72C810C9900E9E6BD /* ManualPriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8E5DA62C810C9900E9E6BD /* ManualPriceFetcher.swift */; };
3AE2D39D2B83338D00DE5F31 /* SatsPriceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D39C2B83338D00DE5F31 /* SatsPriceApp.swift */; };
3AE2D3A22B83338D00DE5F31 /* SatsPrice.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3A02B83338D00DE5F31 /* SatsPrice.xcdatamodeld */; };
3AE2D3A42B83338D00DE5F31 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3A32B83338D00DE5F31 /* ContentView.swift */; };
3AE2D3A62B83338D00DE5F31 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AE2D3A52B83338D00DE5F31 /* Assets.xcassets */; };
3AE2D3AA2B83338D00DE5F31 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AE2D3A92B83338D00DE5F31 /* Preview Assets.xcassets */; };
3AE2D3B42B83338E00DE5F31 /* SatsPriceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3B32B83338E00DE5F31 /* SatsPriceTests.swift */; };
3AE2D3BE2B83338E00DE5F31 /* SatsPriceUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3BD2B83338E00DE5F31 /* SatsPriceUITests.swift */; };
3AE2D3C02B83338E00DE5F31 /* SatsPriceUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3BF2B83338E00DE5F31 /* SatsPriceUITestsLaunchTests.swift */; };
3AE2D3D52B83E58500DE5F31 /* SatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3D42B83E58500DE5F31 /* SatsViewModel.swift */; };
3AE2D3DA2B83FE5F00DE5F31 /* SatsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3D92B83FE5F00DE5F31 /* SatsViewModelTests.swift */; };
3AE2D3DD2B84020200DE5F31 /* BigDecimal in Frameworks */ = {isa = PBXBuildFile; productRef = 3AE2D3DC2B84020200DE5F31 /* BigDecimal */; };
3AE2D3DF2B84597100DE5F31 /* PriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3DE2B84597100DE5F31 /* PriceFetcher.swift */; };
3AE2D3E12B8459D600DE5F31 /* CoinbasePriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3E02B8459D600DE5F31 /* CoinbasePriceFetcher.swift */; };
3AE2D3E32B8461FE00DE5F31 /* CoinGeckoPriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3E22B8461FE00DE5F31 /* CoinGeckoPriceFetcher.swift */; };
3AE2D3E52B846A8600DE5F31 /* PriceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3E42B846A8600DE5F31 /* PriceSource.swift */; };
3AE2D3E72B85690200DE5F31 /* PriceFetcherDelegator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2D3E62B85690200DE5F31 /* PriceFetcherDelegator.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
3AE2D3B02B83338E00DE5F31 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AE2D3912B83338D00DE5F31 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3AE2D3982B83338D00DE5F31;
remoteInfo = SatsPrice;
};
3AE2D3BA2B83338E00DE5F31 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AE2D3912B83338D00DE5F31 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3AE2D3982B83338D00DE5F31;
remoteInfo = SatsPrice;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
3A7B2AA22B86407A00ACC4A7 /* FakePriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakePriceFetcher.swift; sourceTree = "<group>"; };
3A8E5DA62C810C9900E9E6BD /* ManualPriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualPriceFetcher.swift; sourceTree = "<group>"; };
3AE2D3992B83338D00DE5F31 /* SatsPrice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SatsPrice.app; sourceTree = BUILT_PRODUCTS_DIR; };
3AE2D39C2B83338D00DE5F31 /* SatsPriceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SatsPriceApp.swift; sourceTree = "<group>"; };
3AE2D3A12B83338D00DE5F31 /* SatsPrice.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SatsPrice.xcdatamodel; sourceTree = "<group>"; };
3AE2D3A32B83338D00DE5F31 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
3AE2D3A52B83338D00DE5F31 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3AE2D3A72B83338D00DE5F31 /* SatsPrice.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SatsPrice.entitlements; sourceTree = "<group>"; };
3AE2D3A92B83338D00DE5F31 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
3AE2D3AF2B83338E00DE5F31 /* SatsPriceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SatsPriceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3AE2D3B32B83338E00DE5F31 /* SatsPriceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SatsPriceTests.swift; sourceTree = "<group>"; };
3AE2D3B92B83338E00DE5F31 /* SatsPriceUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SatsPriceUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3AE2D3BD2B83338E00DE5F31 /* SatsPriceUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SatsPriceUITests.swift; sourceTree = "<group>"; };
3AE2D3BF2B83338E00DE5F31 /* SatsPriceUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SatsPriceUITestsLaunchTests.swift; sourceTree = "<group>"; };
3AE2D3D42B83E58500DE5F31 /* SatsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SatsViewModel.swift; sourceTree = "<group>"; };
3AE2D3D92B83FE5F00DE5F31 /* SatsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SatsViewModelTests.swift; sourceTree = "<group>"; };
3AE2D3DE2B84597100DE5F31 /* PriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceFetcher.swift; sourceTree = "<group>"; };
3AE2D3E02B8459D600DE5F31 /* CoinbasePriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinbasePriceFetcher.swift; sourceTree = "<group>"; };
3AE2D3E22B8461FE00DE5F31 /* CoinGeckoPriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinGeckoPriceFetcher.swift; sourceTree = "<group>"; };
3AE2D3E42B846A8600DE5F31 /* PriceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceSource.swift; sourceTree = "<group>"; };
3AE2D3E62B85690200DE5F31 /* PriceFetcherDelegator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceFetcherDelegator.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
3AE2D3962B83338D00DE5F31 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3AE2D3DD2B84020200DE5F31 /* BigDecimal in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AE2D3AC2B83338E00DE5F31 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
3AE2D3B62B83338E00DE5F31 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3AE2D3902B83338D00DE5F31 = {
isa = PBXGroup;
children = (
3AE2D39B2B83338D00DE5F31 /* SatsPrice */,
3AE2D3B22B83338E00DE5F31 /* SatsPriceTests */,
3AE2D3BC2B83338E00DE5F31 /* SatsPriceUITests */,
3AE2D39A2B83338D00DE5F31 /* Products */,
);
sourceTree = "<group>";
};
3AE2D39A2B83338D00DE5F31 /* Products */ = {
isa = PBXGroup;
children = (
3AE2D3992B83338D00DE5F31 /* SatsPrice.app */,
3AE2D3AF2B83338E00DE5F31 /* SatsPriceTests.xctest */,
3AE2D3B92B83338E00DE5F31 /* SatsPriceUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
3AE2D39B2B83338D00DE5F31 /* SatsPrice */ = {
isa = PBXGroup;
children = (
3AE2D3E82B856B1800DE5F31 /* Network */,
3AE2D3A32B83338D00DE5F31 /* ContentView.swift */,
3AE2D39C2B83338D00DE5F31 /* SatsPriceApp.swift */,
3AE2D3D42B83E58500DE5F31 /* SatsViewModel.swift */,
3AE2D3A52B83338D00DE5F31 /* Assets.xcassets */,
3AE2D3A72B83338D00DE5F31 /* SatsPrice.entitlements */,
3AE2D3A02B83338D00DE5F31 /* SatsPrice.xcdatamodeld */,
3AE2D3A82B83338D00DE5F31 /* Preview Content */,
);
path = SatsPrice;
sourceTree = "<group>";
};
3AE2D3A82B83338D00DE5F31 /* Preview Content */ = {
isa = PBXGroup;
children = (
3AE2D3A92B83338D00DE5F31 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
3AE2D3B22B83338E00DE5F31 /* SatsPriceTests */ = {
isa = PBXGroup;
children = (
3AE2D3B32B83338E00DE5F31 /* SatsPriceTests.swift */,
3AE2D3D92B83FE5F00DE5F31 /* SatsViewModelTests.swift */,
);
path = SatsPriceTests;
sourceTree = "<group>";
};
3AE2D3BC2B83338E00DE5F31 /* SatsPriceUITests */ = {
isa = PBXGroup;
children = (
3AE2D3BD2B83338E00DE5F31 /* SatsPriceUITests.swift */,
3AE2D3BF2B83338E00DE5F31 /* SatsPriceUITestsLaunchTests.swift */,
);
path = SatsPriceUITests;
sourceTree = "<group>";
};
3AE2D3E82B856B1800DE5F31 /* Network */ = {
isa = PBXGroup;
children = (
3AE2D3E02B8459D600DE5F31 /* CoinbasePriceFetcher.swift */,
3AE2D3E22B8461FE00DE5F31 /* CoinGeckoPriceFetcher.swift */,
3A7B2AA22B86407A00ACC4A7 /* FakePriceFetcher.swift */,
3A8E5DA62C810C9900E9E6BD /* ManualPriceFetcher.swift */,
3AE2D3DE2B84597100DE5F31 /* PriceFetcher.swift */,
3AE2D3E62B85690200DE5F31 /* PriceFetcherDelegator.swift */,
3AE2D3E42B846A8600DE5F31 /* PriceSource.swift */,
);
path = Network;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
3AE2D3982B83338D00DE5F31 /* SatsPrice */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3AE2D3C32B83338E00DE5F31 /* Build configuration list for PBXNativeTarget "SatsPrice" */;
buildPhases = (
3AE2D3952B83338D00DE5F31 /* Sources */,
3AE2D3962B83338D00DE5F31 /* Frameworks */,
3AE2D3972B83338D00DE5F31 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SatsPrice;
packageProductDependencies = (
3AE2D3DC2B84020200DE5F31 /* BigDecimal */,
);
productName = SatsPrice;
productReference = 3AE2D3992B83338D00DE5F31 /* SatsPrice.app */;
productType = "com.apple.product-type.application";
};
3AE2D3AE2B83338E00DE5F31 /* SatsPriceTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3AE2D3C62B83338E00DE5F31 /* Build configuration list for PBXNativeTarget "SatsPriceTests" */;
buildPhases = (
3AE2D3AB2B83338E00DE5F31 /* Sources */,
3AE2D3AC2B83338E00DE5F31 /* Frameworks */,
3AE2D3AD2B83338E00DE5F31 /* Resources */,
);
buildRules = (
);
dependencies = (
3AE2D3B12B83338E00DE5F31 /* PBXTargetDependency */,
);
name = SatsPriceTests;
productName = SatsPriceTests;
productReference = 3AE2D3AF2B83338E00DE5F31 /* SatsPriceTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
3AE2D3B82B83338E00DE5F31 /* SatsPriceUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3AE2D3C92B83338E00DE5F31 /* Build configuration list for PBXNativeTarget "SatsPriceUITests" */;
buildPhases = (
3AE2D3B52B83338E00DE5F31 /* Sources */,
3AE2D3B62B83338E00DE5F31 /* Frameworks */,
3AE2D3B72B83338E00DE5F31 /* Resources */,
);
buildRules = (
);
dependencies = (
3AE2D3BB2B83338E00DE5F31 /* PBXTargetDependency */,
);
name = SatsPriceUITests;
productName = SatsPriceUITests;
productReference = 3AE2D3B92B83338E00DE5F31 /* SatsPriceUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
3AE2D3912B83338D00DE5F31 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1520;
LastUpgradeCheck = 1520;
TargetAttributes = {
3AE2D3982B83338D00DE5F31 = {
CreatedOnToolsVersion = 15.2;
};
3AE2D3AE2B83338E00DE5F31 = {
CreatedOnToolsVersion = 15.2;
TestTargetID = 3AE2D3982B83338D00DE5F31;
};
3AE2D3B82B83338E00DE5F31 = {
CreatedOnToolsVersion = 15.2;
TestTargetID = 3AE2D3982B83338D00DE5F31;
};
};
};
buildConfigurationList = 3AE2D3942B83338D00DE5F31 /* Build configuration list for PBXProject "SatsPrice" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 3AE2D3902B83338D00DE5F31;
packageReferences = (
3AE2D3DB2B84020200DE5F31 /* XCRemoteSwiftPackageReference "BigDecimal" */,
);
productRefGroup = 3AE2D39A2B83338D00DE5F31 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3AE2D3982B83338D00DE5F31 /* SatsPrice */,
3AE2D3AE2B83338E00DE5F31 /* SatsPriceTests */,
3AE2D3B82B83338E00DE5F31 /* SatsPriceUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
3AE2D3972B83338D00DE5F31 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AE2D3AA2B83338D00DE5F31 /* Preview Assets.xcassets in Resources */,
3AE2D3A62B83338D00DE5F31 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AE2D3AD2B83338E00DE5F31 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
3AE2D3B72B83338E00DE5F31 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
3AE2D3952B83338D00DE5F31 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AE2D3E52B846A8600DE5F31 /* PriceSource.swift in Sources */,
3AE2D3E72B85690200DE5F31 /* PriceFetcherDelegator.swift in Sources */,
3A7B2AA32B86407A00ACC4A7 /* FakePriceFetcher.swift in Sources */,
3AE2D3E12B8459D600DE5F31 /* CoinbasePriceFetcher.swift in Sources */,
3AE2D39D2B83338D00DE5F31 /* SatsPriceApp.swift in Sources */,
3AE2D3A22B83338D00DE5F31 /* SatsPrice.xcdatamodeld in Sources */,
3AE2D3A42B83338D00DE5F31 /* ContentView.swift in Sources */,
3A8E5DA72C810C9900E9E6BD /* ManualPriceFetcher.swift in Sources */,
3AE2D3D52B83E58500DE5F31 /* SatsViewModel.swift in Sources */,
3AE2D3E32B8461FE00DE5F31 /* CoinGeckoPriceFetcher.swift in Sources */,
3AE2D3DF2B84597100DE5F31 /* PriceFetcher.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AE2D3AB2B83338E00DE5F31 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AE2D3DA2B83FE5F00DE5F31 /* SatsViewModelTests.swift in Sources */,
3AE2D3B42B83338E00DE5F31 /* SatsPriceTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AE2D3B52B83338E00DE5F31 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AE2D3BE2B83338E00DE5F31 /* SatsPriceUITests.swift in Sources */,
3AE2D3C02B83338E00DE5F31 /* SatsPriceUITestsLaunchTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
3AE2D3B12B83338E00DE5F31 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3AE2D3982B83338D00DE5F31 /* SatsPrice */;
targetProxy = 3AE2D3B02B83338E00DE5F31 /* PBXContainerItemProxy */;
};
3AE2D3BB2B83338E00DE5F31 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3AE2D3982B83338D00DE5F31 /* SatsPrice */;
targetProxy = 3AE2D3BA2B83338E00DE5F31 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
3AE2D3C12B83338E00DE5F31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos";
SUPPORTS_MACCATALYST = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
3AE2D3C22B83338E00DE5F31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos";
SUPPORTS_MACCATALYST = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
3AE2D3C42B83338E00DE5F31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SatsPrice/SatsPrice.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"SatsPrice/Preview Content\"";
DEVELOPMENT_TEAM = S99A5B637C;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SatsPrice;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPrice;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
3AE2D3C52B83338E00DE5F31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SatsPrice/SatsPrice.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"SatsPrice/Preview Content\"";
DEVELOPMENT_TEAM = S99A5B637C;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SatsPrice;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPrice;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
3AE2D3C72B83338E00DE5F31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPriceTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SatsPrice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SatsPrice";
};
name = Debug;
};
3AE2D3C82B83338E00DE5F31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPriceTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SatsPrice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SatsPrice";
};
name = Release;
};
3AE2D3CA2B83338E00DE5F31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPriceUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = SatsPrice;
};
name = Debug;
};
3AE2D3CB2B83338E00DE5F31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPriceUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = SatsPrice;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3AE2D3942B83338D00DE5F31 /* Build configuration list for PBXProject "SatsPrice" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AE2D3C12B83338E00DE5F31 /* Debug */,
3AE2D3C22B83338E00DE5F31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3AE2D3C32B83338E00DE5F31 /* Build configuration list for PBXNativeTarget "SatsPrice" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AE2D3C42B83338E00DE5F31 /* Debug */,
3AE2D3C52B83338E00DE5F31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3AE2D3C62B83338E00DE5F31 /* Build configuration list for PBXNativeTarget "SatsPriceTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AE2D3C72B83338E00DE5F31 /* Debug */,
3AE2D3C82B83338E00DE5F31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3AE2D3C92B83338E00DE5F31 /* Build configuration list for PBXNativeTarget "SatsPriceUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AE2D3CA2B83338E00DE5F31 /* Debug */,
3AE2D3CB2B83338E00DE5F31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
3AE2D3DB2B84020200DE5F31 /* XCRemoteSwiftPackageReference "BigDecimal" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mgriebling/BigDecimal.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.2.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
3AE2D3DC2B84020200DE5F31 /* BigDecimal */ = {
isa = XCSwiftPackageProductDependency;
package = 3AE2D3DB2B84020200DE5F31 /* XCRemoteSwiftPackageReference "BigDecimal" */;
productName = BigDecimal;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
3AE2D3A02B83338D00DE5F31 /* SatsPrice.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
3AE2D3A12B83338D00DE5F31 /* SatsPrice.xcdatamodel */,
);
currentVersion = 3AE2D3A12B83338D00DE5F31 /* SatsPrice.xcdatamodel */;
path = SatsPrice.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = 3AE2D3912B83338D00DE5F31 /* Project object */;
}

View File

@@ -1,32 +0,0 @@
{
"pins" : [
{
"identity" : "bigdecimal",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mgriebling/BigDecimal.git",
"state" : {
"revision" : "c4e8348c7fbc90f29225d5f8681ce33a16ab33a2",
"version" : "2.2.3"
}
},
{
"identity" : "bigint",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mgriebling/BigInt.git",
"state" : {
"revision" : "498d4d290658d7c43a24b9e309c321592dc294f2",
"version" : "2.0.10"
}
},
{
"identity" : "uint128",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mgriebling/UInt128.git",
"state" : {
"revision" : "59dac4f14d657fd60bacfdfb7398d38b450af74f",
"version" : "3.1.5"
}
}
],
"version" : 2
}

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "A2E66E20-4DE4-4B6E-A647-48299476A936"
type = "1"
version = "2.0">
</Bucket>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>Demo (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>Demo (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>Demo (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>SatsPrice.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
//
// PriceFetcher.swift
// SatsPrice
//
// Created by Terry Yiu on 2/19/24.
//
import Foundation
import BigDecimal
protocol PriceFetcher {
func btcToUsd() async throws -> BigDecimal?
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<elements>
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
</elements>
</model>

View File

@@ -1,17 +0,0 @@
//
// SatsPriceApp.swift
// SatsPrice
//
// Created by Terry Yiu on 2/19/24.
//
import SwiftUI
@main
struct SatsPriceApp: App {
var body: some Scene {
WindowGroup {
ContentView(.coinbase)
}
}
}

View File

@@ -1,67 +0,0 @@
//
// SatsViewModel.swift
// SatsPrice
//
// Created by Terry Yiu on 2/19/24.
//
import Foundation
import BigDecimal
class SatsViewModel: ObservableObject {
@Published private(set) var btcToUsd: BigDecimal = BigDecimal.nan
@Published var lastUpdated: Date = Date.now
@Published private(set) var sats: BigDecimal = 0
@Published private(set) var btc: BigDecimal = 0
@Published private(set) var usd: BigDecimal = 0
var btcToUsdString: String {
get { btcToUsd.asString(.plain) }
set {
self.btcToUsd = BigDecimal(newValue)
self.usd = btc.multiply(btcToUsd, Rounding(.down, 20))
}
}
var satsString: String {
get { sats.asString(.plain) }
set {
self.sats = BigDecimal(newValue)
let preciseDivide = sats.divide(100000000)
if preciseDivide.isNaN {
self.btc = sats.divide(100000000, Rounding(.down, 20))
} else {
self.btc = sats.divide(100000000)
}
self.usd = btc.multiply(btcToUsd, Rounding(.down, 20))
}
}
var btcString: String {
get { btc.asString(.plain) }
set {
self.btc = BigDecimal(newValue)
self.sats = btc.multiply(100000000, Rounding(.down, 20))
self.usd = btc.multiply(btcToUsd, Rounding(.down, 20))
}
}
var usdString: String {
get { usd.asString(.plain) }
set {
self.usd = BigDecimal(newValue)
let preciseDivide = usd.divide(btcToUsd)
if preciseDivide.isNaN {
self.btc = usd.divide(btcToUsd, Rounding(.down, 20))
} else {
self.btc = usd.divide(btcToUsd)
}
self.sats = btc.multiply(100000000, Rounding(.down, 20))
}
}
}

View File

@@ -1,35 +0,0 @@
//
// SatsPriceTests.swift
// SatsPriceTests
//
// Created by Terry Yiu on 2/19/24.
//
import XCTest
final class SatsPriceTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
}
}

View File

@@ -1,41 +0,0 @@
//
// SatsPriceUITests.swift
// SatsPriceUITests
//
// Created by Terry Yiu on 2/19/24.
//
import XCTest
final class SatsPriceUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}

View File

@@ -1,32 +0,0 @@
//
// SatsPriceUITestsLaunchTests.swift
// SatsPriceUITests
//
// Created by Terry Yiu on 2/19/24.
//
import XCTest
final class SatsPriceUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

20
Skip.env Normal file
View File

@@ -0,0 +1,20 @@
// The configuration file for your Skip App (https://skip.tools).
// Properties specified here are shared between
// Darwin/SatsPrice.xcconfig and Android/settings.gradle.kts
// and will be included in the app's metadata files
// Info.plist and AndroidManifest.xml
// PRODUCT_NAME is the default title of the app, which must match the app's Swift module name
PRODUCT_NAME = SatsPrice
// PRODUCT_BUNDLE_IDENTIFIER is the unique id for both the iOS and Android app
PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPrice
// The semantic version of the app
MARKETING_VERSION = 0.2.0
// The build number specifying the internal app version
CURRENT_PROJECT_VERSION = 1
// The package name for the Android entry point, referenced by the AndroidManifest.xml
ANDROID_PACKAGE_NAME = sats.price

View File

@@ -1,15 +1,11 @@
//
// ContentView.swift
// SatsPrice
//
// Created by Terry Yiu on 2/19/24.
//
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
import SwiftUI
import BigDecimal
import Combine
import SwiftUI
struct ContentView: View {
public struct ContentView: View {
@ObservedObject private var satsViewModel = SatsViewModel()
@State private var priceSource: PriceSource
@@ -42,7 +38,7 @@ struct ContentView: View {
satsViewModel.lastUpdated = Date.now
}
var body: some View {
public var body: some View {
Form {
Section {
Picker("Price Source", selection: $priceSource) {
@@ -54,7 +50,7 @@ struct ContentView: View {
HStack {
TextField("", text: $satsViewModel.btcToUsdString)
.disabled(priceSource != .manual)
#if os(iOS)
#if os(iOS) || SKIP
.keyboardType(.decimalPad)
#endif
if priceSource != .manual {
@@ -63,19 +59,21 @@ struct ContentView: View {
await updatePrice()
}
}) {
Image(systemName: "arrow.clockwise")
Image(systemName: "arrow.clockwise.circle")
}
}
}
} header: {
Text("1 BTC to USD")
} footer: {
if priceSource != .manual {
Text("Last updated: \(dateFormatter.string(from: satsViewModel.lastUpdated))")
}
}
Section {
TextField("", text: $satsViewModel.satsString)
#if os(iOS)
#if os(iOS) || SKIP
.keyboardType(.numberPad)
#endif
} header: {
@@ -84,7 +82,7 @@ struct ContentView: View {
Section {
TextField("", text: $satsViewModel.btcString)
#if os(iOS)
#if os(iOS) || SKIP
.keyboardType(.decimalPad)
#endif
} header: {
@@ -93,7 +91,7 @@ struct ContentView: View {
Section {
TextField("", text: $satsViewModel.usdString)
#if os(iOS)
#if os(iOS) || SKIP
.keyboardType(.decimalPad)
#endif
} header: {

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// CoinGeckoPriceFetcher.swift
// SatsPrice
@@ -6,20 +9,23 @@
//
import Foundation
import BigDecimal
private struct CoinGeckoPriceResponse: Codable {
let bitcoin: CoinGeckoPrice
}
private struct CoinGeckoPrice: Codable {
#if !SKIP
let usd: Decimal
#else
let usd: String
#endif
}
class CoinGeckoPriceFetcher : PriceFetcher {
private static let urlString = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&precision=18"
func btcToUsd() async throws -> BigDecimal? {
func btcToUsd() async throws -> Decimal? {
do {
guard let urlComponents = URLComponents(string: CoinGeckoPriceFetcher.urlString), let url = urlComponents.url else {
return nil
@@ -30,7 +36,11 @@ class CoinGeckoPriceFetcher : PriceFetcher {
let priceResponse = try JSONDecoder().decode(CoinGeckoPriceResponse.self, from: data)
let price = priceResponse.bitcoin
return BigDecimal(price.usd)
#if !SKIP
return price.usd
#else
return Decimal(price.usd)
#endif
} catch {
return nil
}

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// CoinbasePriceFetcher.swift
// SatsPrice
@@ -6,7 +9,6 @@
//
import Foundation
import BigDecimal
private struct CoinbasePriceResponse: Codable {
let data: CoinbasePrice
@@ -23,7 +25,7 @@ class CoinbasePriceFetcher : PriceFetcher {
private static let btc = "BTC"
private static let usd = "USD"
func btcToUsd() async throws -> BigDecimal? {
func btcToUsd() async throws -> Decimal? {
do {
guard let urlComponents = URLComponents(string: CoinbasePriceFetcher.urlString), let url = urlComponents.url else {
return nil
@@ -38,7 +40,11 @@ class CoinbasePriceFetcher : PriceFetcher {
return nil
}
return BigDecimal(coinbasePrice.amount)
#if !SKIP
return Decimal(string: coinbasePrice.amount)
#else
return Decimal(coinbasePrice.amount)
#endif
} catch {
return nil
}

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// FakePriceFetcher.swift
// SatsPrice
@@ -7,12 +10,11 @@
#if DEBUG
import Foundation
import BigDecimal
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
class FakePriceFetcher: PriceFetcher {
func btcToUsd() async throws -> BigDecimal? {
BigDecimal(Double.random(in: 10000...100000))
func btcToUsd() async throws -> Decimal? {
Decimal(Double.random(in: 10000...100000))
}
}
#endif

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// ManualPriceFetcher.swift
// SatsPrice
@@ -6,13 +9,12 @@
//
import Foundation
import BigDecimal
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
class ManualPriceFetcher: PriceFetcher {
var price: BigDecimal = 1
var price: Decimal = Decimal(1)
func btcToUsd() async throws -> BigDecimal? {
func btcToUsd() async throws -> Decimal? {
return price
}
}

View File

@@ -0,0 +1,15 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// PriceFetcher.swift
// SatsPrice
//
// Created by Terry Yiu on 2/19/24.
//
import Foundation
protocol PriceFetcher {
func btcToUsd() async throws -> Decimal?
}

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// PriceFetcherDelegator.swift
// SatsPrice
@@ -6,7 +9,6 @@
//
import Foundation
import BigDecimal
class PriceFetcherDelegator: PriceFetcher {
private let coinbasePriceFetcher = CoinbasePriceFetcher()
@@ -37,7 +39,7 @@ class PriceFetcherDelegator: PriceFetcher {
}
}
func btcToUsd() async throws -> BigDecimal? {
func btcToUsd() async throws -> Decimal? {
return try await delegate.btcToUsd()
}
}

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// PriceSource.swift
// SatsPrice

View File

@@ -0,0 +1,27 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
},
"1 BTC to USD" : {
},
"BTC" : {
},
"Last updated: %@" : {
},
"Price Source" : {
},
"Sats" : {
},
"USD" : {
}
},
"version" : "1.0"
}

View File

@@ -0,0 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
public class SatsPriceModule {
}

View File

@@ -0,0 +1,43 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
import Foundation
import OSLog
import SwiftUI
let logger: Logger = Logger(subsystem: "xyz.tyiu.SatsPrice", category: "SatsPrice")
/// The Android SDK number we are running against, or `nil` if not running on Android
let androidSDK = ProcessInfo.processInfo.environment["android.os.Build.VERSION.SDK_INT"].flatMap({ Int($0) })
/// The shared top-level view for the app, loaded from the platform-specific App delegates below.
///
/// The default implementation merely loads the `ContentView` for the app and logs a message.
public struct RootView : View {
public init() {
}
public var body: some View {
ContentView(.coinbase)
.task {
logger.log("Welcome to Skip on \(androidSDK != nil ? "Android" : "Darwin")!")
logger.warning("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat")
}
}
}
#if !SKIP
public protocol SatsPriceApp : App {
}
/// The entry point to the SatsPrice app.
/// The concrete implementation is in the SatsPriceApp module.
public extension SatsPriceApp {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
#endif

View File

@@ -0,0 +1,173 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
//
// SatsViewModel.swift
// SatsPrice
//
// Created by Terry Yiu on 2/19/24.
//
import Foundation
import SwiftUI
class SatsViewModel: ObservableObject {
@Published var lastUpdated: Date = Date.now
@Published var btcToUsdStringInternal: String = ""
@Published var satsStringInternal: String = ""
@Published var btcStringInternal: String = ""
@Published var usdStringInternal: String = ""
var btcToUsdString: String {
get {
btcToUsdStringInternal
}
set {
btcToUsdStringInternal = newValue
if let btc, let btcToUsd {
usdStringInternal = (btc * btcToUsd).formatString()
} else {
usdStringInternal = ""
}
}
}
var satsString: String {
get {
satsStringInternal
}
set {
satsStringInternal = newValue
if let sats {
#if !SKIP
let btc = sats / Decimal(100000000)
#else
let btc = sats.divide(Decimal(100000000), 20, java.math.RoundingMode.DOWN)
#endif
btcStringInternal = btc.formatString()
if let btcToUsd {
usdStringInternal = (btc * btcToUsd).formatString()
} else {
usdStringInternal = ""
}
} else {
btcStringInternal = ""
usdStringInternal = ""
}
}
}
var btcString: String {
get {
btcStringInternal
}
set {
btcStringInternal = newValue
if let btc {
let sats = btc * Decimal(100000000)
satsStringInternal = sats.formatString()
if let btcToUsd {
usdStringInternal = (btc * btcToUsd).formatString()
} else {
usdStringInternal = ""
}
} else {
satsStringInternal = ""
usdStringInternal = ""
}
}
}
var usdString: String {
get {
usdStringInternal
}
set {
usdStringInternal = newValue
if let usd {
if let btcToUsd {
#if !SKIP
let btc = usd / btcToUsd
#else
let btc = usd.divide(btcToUsd, 20, java.math.RoundingMode.DOWN)
#endif
btcStringInternal = btc.formatString()
let sats = btc * Decimal(100000000)
satsStringInternal = sats.formatString()
} else {
satsStringInternal = ""
usdStringInternal = ""
}
} else {
satsStringInternal = ""
usdStringInternal = ""
}
}
}
var btcToUsd: Decimal? {
#if !SKIP
return Decimal(string: btcToUsdStringInternal)
#else
do {
return Decimal(btcToUsdStringInternal)
} catch {
return nil
}
#endif
}
var sats: Decimal? {
#if !SKIP
return Decimal(string: satsStringInternal)
#else
do {
return Decimal(satsStringInternal)
} catch {
return nil
}
#endif
}
var btc: Decimal? {
#if !SKIP
return Decimal(string: btcStringInternal)
#else
do {
return Decimal(btcStringInternal)
} catch {
return nil
}
#endif
}
var usd: Decimal? {
#if !SKIP
return Decimal(string: usdStringInternal)
#else
do {
return Decimal(usdStringInternal)
} catch {
return nil
}
#endif
}
}
extension Decimal {
func formatString() -> String {
#if !SKIP
return String(describing: self)
#else
return stripTrailingZeros().toPlainString()
#endif
}
}

View File

@@ -0,0 +1,3 @@
# Configuration file for https://skip.tools project
build:
contents:

View File

@@ -1,3 +1,6 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU General Public License 3.0
// as published by the Free Software Foundation https://fsf.org
//
// SatsViewModelTests.swift
// SatsPriceTests
@@ -6,66 +9,57 @@
//
import XCTest
import BigDecimal
@testable import SatsPrice
final class SatsViewModelTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testSatsViewModel() {
let satsViewModel = SatsViewModel()
satsViewModel.btcToUsdString = "54321"
// Test BTC updates.
satsViewModel.btcString = "1"
XCTAssertEqual(satsViewModel.btc, BigDecimal("1"))
XCTAssertEqual(satsViewModel.btc, Decimal(string: "1"))
XCTAssertEqual(satsViewModel.btcString, "1")
XCTAssertEqual(satsViewModel.sats, BigDecimal("100000000"))
XCTAssertEqual(satsViewModel.sats, Decimal(string: "100000000"))
XCTAssertEqual(satsViewModel.satsString, "100000000")
XCTAssertEqual(satsViewModel.usd, BigDecimal("54321"))
XCTAssertEqual(satsViewModel.usd, Decimal(string: "54321"))
XCTAssertEqual(satsViewModel.usdString, "54321")
// Test Sats updates.
satsViewModel.satsString = "200000000"
XCTAssertEqual(satsViewModel.btc, BigDecimal("2"))
XCTAssertEqual(satsViewModel.btc, Decimal(string: "2"))
XCTAssertEqual(satsViewModel.btcString, "2")
XCTAssertEqual(satsViewModel.sats, BigDecimal("200000000"))
XCTAssertEqual(satsViewModel.sats, Decimal(string: "200000000"))
XCTAssertEqual(satsViewModel.satsString, "200000000")
XCTAssertEqual(satsViewModel.usd, BigDecimal("108642"))
XCTAssertEqual(satsViewModel.usd, Decimal(string: "108642"))
XCTAssertEqual(satsViewModel.usdString, "108642")
// Test USD updates.
satsViewModel.usdString = "162963"
XCTAssertEqual(satsViewModel.btc, BigDecimal("3"))
XCTAssertEqual(satsViewModel.btc, Decimal(string: "3"))
XCTAssertEqual(satsViewModel.btcString, "3")
XCTAssertEqual(satsViewModel.sats, BigDecimal("300000000"))
XCTAssertEqual(satsViewModel.sats, Decimal(string: "300000000"))
XCTAssertEqual(satsViewModel.satsString, "300000000")
XCTAssertEqual(satsViewModel.usd, BigDecimal("162963"))
XCTAssertEqual(satsViewModel.usd, Decimal(string: "162963"))
XCTAssertEqual(satsViewModel.usdString, "162963")
// Test fractional amounts.
satsViewModel.usdString = "1"
XCTAssertEqual(satsViewModel.btc, BigDecimal("0.000018409086725207562452"))
XCTAssertEqual(satsViewModel.btcString, "0.000018409086725207562452")
XCTAssertEqual(satsViewModel.sats, BigDecimal("1840.9086725207562452"))
XCTAssertEqual(satsViewModel.satsString, "1840.9086725207562452")
XCTAssertEqual(satsViewModel.usd, BigDecimal("1"))
XCTAssertEqual(satsViewModel.btc, Decimal(string: "0.00001840908672520756245282671526665562"))
XCTAssertEqual(satsViewModel.btcString, "0.00001840908672520756245282671526665562")
XCTAssertEqual(satsViewModel.sats, Decimal(string: "1840.908672520756245282671526665562"))
XCTAssertEqual(satsViewModel.satsString, "1840.908672520756245282671526665562")
XCTAssertEqual(satsViewModel.usd, Decimal(string: "1"))
XCTAssertEqual(satsViewModel.usdString, "1")
// Test large amounts that exceed the cap of 21M BTC.
satsViewModel.usdString = "11407419999999"
XCTAssertEqual(satsViewModel.btc, BigDecimal("210000184.09084884298"))
XCTAssertEqual(satsViewModel.btcString, "210000184.09084884298")
XCTAssertEqual(satsViewModel.sats, BigDecimal("21000018409084884.298"))
XCTAssertEqual(satsViewModel.satsString, "21000018409084884.298")
XCTAssertEqual(satsViewModel.usd, BigDecimal("11407419999999"))
XCTAssertEqual(satsViewModel.btc, Decimal(string: "210000184.09084884298889932070469983984"))
XCTAssertEqual(satsViewModel.btcString, "210000184.09084884298889932070469983984")
XCTAssertEqual(satsViewModel.sats, Decimal(string: "21000018409084884.298889932070469983984"))
XCTAssertEqual(satsViewModel.satsString, "21000018409084884.298889932070469983984")
XCTAssertEqual(satsViewModel.usd, Decimal(string: "11407419999999"))
XCTAssertEqual(satsViewModel.usdString, "11407419999999")
}

View File

@@ -0,0 +1,3 @@
# Configuration file for https://skip.tools project
#build:
# contents:

Some files were not shown because too many files have changed in this diff Show More