@tastic/profile 0.1.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +42 -2
- package/dist/index.mjs +45 -1
- package/expo-module.config.json +6 -0
- package/ios/TasticProfile.podspec +26 -0
- package/ios/TasticProfileModule.swift +22 -0
- package/package.json +12 -3
- package/src/index.ts +1 -0
- package/src/sharedProfileStore.ts +65 -0
package/README.md
CHANGED
|
@@ -54,6 +54,38 @@ see its own doc comment for why that component is generic over your own `Profile
|
|
|
54
54
|
inline in the row you tap, with a destructive-delete confirmation. No navigation of its own (no
|
|
55
55
|
router, no back button) — a host app renders this as a routed screen's body and supplies its own
|
|
56
56
|
persistence via `profiles`/`onCreate`/`onSave`/`onDelete`.
|
|
57
|
+
- **`loadSharedProfiles` / `saveSharedProfiles` / `isSharedProfileStoreAvailable`** — an *optional*
|
|
58
|
+
cross-app roster, shared between multiple apps of yours via a native iOS App Group (same Apple
|
|
59
|
+
Developer Team, same `"com.apple.security.application-groups"` entitlement value on every
|
|
60
|
+
participating app). This only ever carries the base `Profile` shape above — a host app's own
|
|
61
|
+
extension fields (a key scheme, a control scheme, anything else) are NOT part of it and have to
|
|
62
|
+
keep living in that app's own *local* storage, keyed by profile id, merged onto the shared base
|
|
63
|
+
roster at read time (falling back to some per-app default whenever a shared profile's id isn't
|
|
64
|
+
in that app's own local extension table yet — e.g. it was created on a different app in the
|
|
65
|
+
group). This split is deliberate, not a limitation to work around: it's this package's own
|
|
66
|
+
base-fields-only `Profile` boundary (see above), just applied to storage instead of just to the
|
|
67
|
+
TypeScript type.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// App-side sketch — see BoxHockey's or LightCycles' own useProfiles.tsx for a full example.
|
|
71
|
+
const GROUP_ID = 'group.com.yourteam.yourgames'
|
|
72
|
+
const base = isSharedProfileStoreAvailable ? await loadSharedProfiles(GROUP_ID) : localBaseFallback
|
|
73
|
+
const profiles = base.map((p) => ({ ...p, controlScheme: localExtensions[p.id]?.controlScheme ?? DEFAULT_CONTROL_SCHEME }))
|
|
74
|
+
// ...on create/update/delete, write the base fields back with saveSharedProfiles(GROUP_ID, nextBase)
|
|
75
|
+
// and the extension field to the app's own local storage, same as today.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Backed by a small native Expo Module (`ios/TasticProfileModule.swift`) wrapping
|
|
79
|
+
`UserDefaults(suiteName:)` — iOS only (see `expo-module.config.json`'s `"apple"`-only platform
|
|
80
|
+
list); `isSharedProfileStoreAvailable` is `false` on Android/web and on any iOS build that hasn't
|
|
81
|
+
run `expo prebuild` since adding this App Group's entitlement, so a host app always needs its own
|
|
82
|
+
local-storage fallback path regardless of platform. Requires the App Group itself to be declared
|
|
83
|
+
in each consuming app's own Expo config, e.g.:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
// app.json
|
|
87
|
+
{ "expo": { "ios": { "entitlements": { "com.apple.security.application-groups": ["group.com.yourteam.yourgames"] } } } }
|
|
88
|
+
```
|
|
57
89
|
|
|
58
90
|
## Install (local dev via yalc)
|
|
59
91
|
|
package/dist/index.d.mts
CHANGED
|
@@ -55,4 +55,8 @@ declare const MAX_TAG_LENGTH = 3;
|
|
|
55
55
|
declare function isValidTag(value: unknown): value is string;
|
|
56
56
|
declare function isValidProfile(value: unknown): value is Profile;
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
declare const isSharedProfileStoreAvailable: boolean;
|
|
59
|
+
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
60
|
+
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
61
|
+
|
|
62
|
+
export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
|
package/dist/index.d.ts
CHANGED
|
@@ -55,4 +55,8 @@ declare const MAX_TAG_LENGTH = 3;
|
|
|
55
55
|
declare function isValidTag(value: unknown): value is string;
|
|
56
56
|
declare function isValidProfile(value: unknown): value is Profile;
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
declare const isSharedProfileStoreAvailable: boolean;
|
|
59
|
+
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
60
|
+
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
61
|
+
|
|
62
|
+
export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
|
package/dist/index.js
CHANGED
|
@@ -25,8 +25,11 @@ __export(index_exports, {
|
|
|
25
25
|
ProfileChip: () => ProfileChip,
|
|
26
26
|
ProfilePicker: () => ProfilePicker,
|
|
27
27
|
ProfilesManager: () => ProfilesManager,
|
|
28
|
+
isSharedProfileStoreAvailable: () => isSharedProfileStoreAvailable,
|
|
28
29
|
isValidProfile: () => isValidProfile,
|
|
29
|
-
isValidTag: () => isValidTag
|
|
30
|
+
isValidTag: () => isValidTag,
|
|
31
|
+
loadSharedProfiles: () => loadSharedProfiles,
|
|
32
|
+
saveSharedProfiles: () => saveSharedProfiles
|
|
30
33
|
});
|
|
31
34
|
module.exports = __toCommonJS(index_exports);
|
|
32
35
|
|
|
@@ -503,6 +506,40 @@ var styles3 = import_react_native3.StyleSheet.create({
|
|
|
503
506
|
fontWeight: "bold"
|
|
504
507
|
}
|
|
505
508
|
});
|
|
509
|
+
|
|
510
|
+
// src/sharedProfileStore.ts
|
|
511
|
+
var native = null;
|
|
512
|
+
try {
|
|
513
|
+
const { requireOptionalNativeModule } = require("expo-modules-core");
|
|
514
|
+
native = requireOptionalNativeModule("TasticProfile") ?? null;
|
|
515
|
+
} catch {
|
|
516
|
+
native = null;
|
|
517
|
+
}
|
|
518
|
+
var PROFILES_KEY = "sharedProfiles";
|
|
519
|
+
var isSharedProfileStoreAvailable = native !== null;
|
|
520
|
+
async function loadSharedProfiles(groupId) {
|
|
521
|
+
if (!native) return [];
|
|
522
|
+
let json = null;
|
|
523
|
+
try {
|
|
524
|
+
json = await native.getSharedJSON(groupId, PROFILES_KEY);
|
|
525
|
+
} catch {
|
|
526
|
+
return [];
|
|
527
|
+
}
|
|
528
|
+
if (!json) return [];
|
|
529
|
+
try {
|
|
530
|
+
const parsed = JSON.parse(json);
|
|
531
|
+
return Array.isArray(parsed) ? parsed.filter(isValidProfile) : [];
|
|
532
|
+
} catch {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function saveSharedProfiles(groupId, profiles) {
|
|
537
|
+
if (!native) return;
|
|
538
|
+
try {
|
|
539
|
+
await native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify(profiles));
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
506
543
|
// Annotate the CommonJS export names for ESM import in node:
|
|
507
544
|
0 && (module.exports = {
|
|
508
545
|
MAX_PROFILE_NAME_LENGTH,
|
|
@@ -510,6 +547,9 @@ var styles3 = import_react_native3.StyleSheet.create({
|
|
|
510
547
|
ProfileChip,
|
|
511
548
|
ProfilePicker,
|
|
512
549
|
ProfilesManager,
|
|
550
|
+
isSharedProfileStoreAvailable,
|
|
513
551
|
isValidProfile,
|
|
514
|
-
isValidTag
|
|
552
|
+
isValidTag,
|
|
553
|
+
loadSharedProfiles,
|
|
554
|
+
saveSharedProfiles
|
|
515
555
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/ProfileChip.tsx
|
|
2
9
|
import { getContrastColor } from "@rific/auto-paper";
|
|
3
10
|
import { StyleSheet, View } from "react-native";
|
|
@@ -471,12 +478,49 @@ var styles3 = StyleSheet3.create({
|
|
|
471
478
|
fontWeight: "bold"
|
|
472
479
|
}
|
|
473
480
|
});
|
|
481
|
+
|
|
482
|
+
// src/sharedProfileStore.ts
|
|
483
|
+
var native = null;
|
|
484
|
+
try {
|
|
485
|
+
const { requireOptionalNativeModule } = __require("expo-modules-core");
|
|
486
|
+
native = requireOptionalNativeModule("TasticProfile") ?? null;
|
|
487
|
+
} catch {
|
|
488
|
+
native = null;
|
|
489
|
+
}
|
|
490
|
+
var PROFILES_KEY = "sharedProfiles";
|
|
491
|
+
var isSharedProfileStoreAvailable = native !== null;
|
|
492
|
+
async function loadSharedProfiles(groupId) {
|
|
493
|
+
if (!native) return [];
|
|
494
|
+
let json = null;
|
|
495
|
+
try {
|
|
496
|
+
json = await native.getSharedJSON(groupId, PROFILES_KEY);
|
|
497
|
+
} catch {
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
if (!json) return [];
|
|
501
|
+
try {
|
|
502
|
+
const parsed = JSON.parse(json);
|
|
503
|
+
return Array.isArray(parsed) ? parsed.filter(isValidProfile) : [];
|
|
504
|
+
} catch {
|
|
505
|
+
return [];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function saveSharedProfiles(groupId, profiles) {
|
|
509
|
+
if (!native) return;
|
|
510
|
+
try {
|
|
511
|
+
await native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify(profiles));
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
474
515
|
export {
|
|
475
516
|
MAX_PROFILE_NAME_LENGTH,
|
|
476
517
|
MAX_TAG_LENGTH,
|
|
477
518
|
ProfileChip,
|
|
478
519
|
ProfilePicker,
|
|
479
520
|
ProfilesManager,
|
|
521
|
+
isSharedProfileStoreAvailable,
|
|
480
522
|
isValidProfile,
|
|
481
|
-
isValidTag
|
|
523
|
+
isValidTag,
|
|
524
|
+
loadSharedProfiles,
|
|
525
|
+
saveSharedProfiles
|
|
482
526
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'TasticProfile'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = { :ios => '16.4' }
|
|
14
|
+
s.swift_version = '5.9'
|
|
15
|
+
s.source = { git: package['repository']['url'] }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
s.pod_target_xcconfig = {
|
|
21
|
+
'DEFINES_MODULE' => 'YES',
|
|
22
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
// Generic group/key -> JSON-string bridge onto a native iOS App Group's shared UserDefaults suite
|
|
4
|
+
// — the mechanism that actually lets two separate apps (same Apple Developer Team, both declaring
|
|
5
|
+
// the same "com.apple.security.application-groups" entitlement) read and write one common blob of
|
|
6
|
+
// data. Nothing here is @tastic/profile-specific — see src/sharedProfileStore.ts, the one caller,
|
|
7
|
+
// for the profile-roster-shaped API built on top of this. Android/web never register this module
|
|
8
|
+
// at all (see expo-module.config.json's "apple"-only platform list), so a consuming app has to
|
|
9
|
+
// treat "module not present" as a normal, expected case rather than an error.
|
|
10
|
+
public final class TasticProfileModule: Module {
|
|
11
|
+
public func definition() -> ModuleDefinition {
|
|
12
|
+
Name("TasticProfile")
|
|
13
|
+
|
|
14
|
+
AsyncFunction("getSharedJSON") { (groupId: String, key: String) -> String? in
|
|
15
|
+
UserDefaults(suiteName: groupId)?.string(forKey: key)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
AsyncFunction("setSharedJSON") { (groupId: String, key: String, value: String) -> Void in
|
|
19
|
+
UserDefaults(suiteName: groupId)?.set(value, forKey: key)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Player profile identity for local-multiplayer React Native games — a saved name/color/tag a player selects per seat, with a picker, an inline-editable roster screen, and a reusable identity badge, deliberately blind to any app-specific fields (control scheme, stats) an app layers on top",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -39,13 +39,14 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
41
|
"src",
|
|
42
|
+
"ios",
|
|
43
|
+
"expo-module.config.json",
|
|
42
44
|
"!src/__tests__",
|
|
43
45
|
"!src/__mocks__"
|
|
44
46
|
],
|
|
45
47
|
"scripts": {
|
|
46
48
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
47
49
|
"build:watch": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
48
|
-
"ci": "npm run lint && npm test && npm run typecheck && npm run build",
|
|
49
50
|
"fix": "eslint --fix",
|
|
50
51
|
"lint": "eslint",
|
|
51
52
|
"prepublishOnly": "npm run build",
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"test": "jest",
|
|
57
58
|
"test:watch": "jest --watchAll",
|
|
58
59
|
"typecheck": "tsc --noEmit",
|
|
59
|
-
"
|
|
60
|
+
"verify": "npm run lint && npm test && npm run typecheck && npm run build",
|
|
61
|
+
"preversion": "npm ci && npm run verify"
|
|
60
62
|
},
|
|
61
63
|
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
62
64
|
"devDependencies": {
|
|
@@ -75,6 +77,7 @@
|
|
|
75
77
|
"eslint": "^9.39.4",
|
|
76
78
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
77
79
|
"eslint-plugin-react-native": "^5.0.0",
|
|
80
|
+
"expo-modules-core": "^57.0.14",
|
|
78
81
|
"jest": "^30.4.2",
|
|
79
82
|
"jest-environment-jsdom": "^30.4.1",
|
|
80
83
|
"prettier": "^3.8.3",
|
|
@@ -94,11 +97,17 @@
|
|
|
94
97
|
"@rific/toaster": ">=0.4.0",
|
|
95
98
|
"@tastic/core": ">=0.1.0",
|
|
96
99
|
"@tastic/hud": ">=0.1.3",
|
|
100
|
+
"expo-modules-core": ">=1.0.0",
|
|
97
101
|
"react": ">=19.0.0",
|
|
98
102
|
"react-native": ">=0.76.0",
|
|
99
103
|
"react-native-paper": ">=5.0.0",
|
|
100
104
|
"react-native-safe-area-context": ">=4.0.0"
|
|
101
105
|
},
|
|
106
|
+
"peerDependenciesMeta": {
|
|
107
|
+
"expo-modules-core": {
|
|
108
|
+
"optional": true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
102
111
|
"publishConfig": {
|
|
103
112
|
"access": "public"
|
|
104
113
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { ProfileChip } from './ProfileChip'
|
|
|
2
2
|
export { ProfilePicker } from './ProfilePicker'
|
|
3
3
|
export { ProfilesManager, type ProfilesManagerProps } from './ProfilesManager'
|
|
4
4
|
export { isValidProfile, isValidTag, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH } from './profilesValidation'
|
|
5
|
+
export { isSharedProfileStoreAvailable, loadSharedProfiles, saveSharedProfiles } from './sharedProfileStore'
|
|
5
6
|
export type { Profile } from './types'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isValidProfile } from './profilesValidation'
|
|
2
|
+
import { Profile } from './types'
|
|
3
|
+
|
|
4
|
+
interface NativeTasticProfile {
|
|
5
|
+
getSharedJSON(groupId: string, key: string): Promise<string | null>
|
|
6
|
+
setSharedJSON(groupId: string, key: string, value: string): Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Lazily/optionally resolved — this package has no hard dependency on expo-modules-core (it's a
|
|
10
|
+
// peer, provided by whatever Expo app consumes it), and the native module itself only registers on
|
|
11
|
+
// iOS (see ios/TasticProfileModule.swift and expo-module.config.json's "apple"-only platform list).
|
|
12
|
+
// Guarding both the require and the lookup means Android/web, and an iOS build that hasn't run
|
|
13
|
+
// `expo prebuild` since adding this package yet, both degrade to "sharing unavailable" instead of a
|
|
14
|
+
// hard crash.
|
|
15
|
+
let native: NativeTasticProfile | null = null
|
|
16
|
+
try {
|
|
17
|
+
const { requireOptionalNativeModule } = require('expo-modules-core')
|
|
18
|
+
native = (requireOptionalNativeModule('TasticProfile') ?? null) as NativeTasticProfile | null
|
|
19
|
+
} catch {
|
|
20
|
+
native = null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PROFILES_KEY = 'sharedProfiles'
|
|
24
|
+
|
|
25
|
+
// True once at module load — a consuming app uses this to decide whether to fall back to its own
|
|
26
|
+
// local-only storage for the base roster too (see this package's README's App Group section).
|
|
27
|
+
export const isSharedProfileStoreAvailable = native !== null
|
|
28
|
+
|
|
29
|
+
// Reads the shared base-identity roster out of a given App Group suite. Always resolves to an
|
|
30
|
+
// array, never null/throws — an unavailable store, a missing/empty blob, and a corrupt blob all
|
|
31
|
+
// read the same as "nothing shared yet." Entries that fail isValidProfile are dropped individually
|
|
32
|
+
// rather than invalidating the whole roster, since the base fields validated here are the ONLY
|
|
33
|
+
// thing this store is a source of truth for — a host app's own extension fields (a control scheme,
|
|
34
|
+
// anything else) never travel through it at all; see the README for the base/extension split this
|
|
35
|
+
// is meant to be used with.
|
|
36
|
+
export async function loadSharedProfiles(groupId: string): Promise<Profile[]> {
|
|
37
|
+
if (!native) return []
|
|
38
|
+
let json: string | null = null
|
|
39
|
+
try {
|
|
40
|
+
json = await native.getSharedJSON(groupId, PROFILES_KEY)
|
|
41
|
+
} catch {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
if (!json) return []
|
|
45
|
+
try {
|
|
46
|
+
const parsed: unknown = JSON.parse(json)
|
|
47
|
+
return Array.isArray(parsed) ? parsed.filter(isValidProfile) : []
|
|
48
|
+
} catch {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Overwrites the entire shared roster — same whole-blob-write discipline a host app's own
|
|
54
|
+
// AsyncStorage-backed profile store already uses, not an incremental per-profile update. A write
|
|
55
|
+
// failure (store unavailable, or some other native error) is swallowed rather than thrown: the
|
|
56
|
+
// caller's own local state is already the source of truth for what just changed, and this is a
|
|
57
|
+
// best-effort mirror of it, not the other way around.
|
|
58
|
+
export async function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void> {
|
|
59
|
+
if (!native) return
|
|
60
|
+
try {
|
|
61
|
+
await native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify(profiles))
|
|
62
|
+
} catch {
|
|
63
|
+
// Best-effort — see doc comment above.
|
|
64
|
+
}
|
|
65
|
+
}
|