@leonxin/meetgames 0.1.13 → 0.1.15

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.
Files changed (49) hide show
  1. package/config/meetsdk-ios.json +1 -1
  2. package/dist/android/manifest.d.ts.map +1 -1
  3. package/dist/android/manifest.js +9 -0
  4. package/dist/android/manifest.js.map +1 -1
  5. package/dist/android/meetSdkRemoteGradle.d.ts.map +1 -1
  6. package/dist/android/meetSdkRemoteGradle.js +126 -6
  7. package/dist/android/meetSdkRemoteGradle.js.map +1 -1
  8. package/dist/config/meetSdkRemoteConfig.d.ts +2 -0
  9. package/dist/config/meetSdkRemoteConfig.d.ts.map +1 -1
  10. package/dist/config/meetSdkRemoteConfig.js +14 -1
  11. package/dist/config/meetSdkRemoteConfig.js.map +1 -1
  12. package/dist/ios/channelConfig.d.ts.map +1 -1
  13. package/dist/ios/channelConfig.js +18 -1
  14. package/dist/ios/channelConfig.js.map +1 -1
  15. package/dist/ios/infoPlist.d.ts +0 -1
  16. package/dist/ios/infoPlist.d.ts.map +1 -1
  17. package/dist/ios/infoPlist.js +0 -5
  18. package/dist/ios/infoPlist.js.map +1 -1
  19. package/dist/ios/integrate.d.ts.map +1 -1
  20. package/dist/ios/integrate.js +172 -97
  21. package/dist/ios/integrate.js.map +1 -1
  22. package/dist/ios/pbxprojEditor.d.ts +1 -1
  23. package/dist/ios/pbxprojEditor.d.ts.map +1 -1
  24. package/dist/ios/pbxprojEditor.js +88 -15
  25. package/dist/ios/pbxprojEditor.js.map +1 -1
  26. package/dist/ops/fileStore.d.ts.map +1 -1
  27. package/dist/ops/fileStore.js +6 -5
  28. package/dist/ops/fileStore.js.map +1 -1
  29. package/dist/ops/handlers.d.ts.map +1 -1
  30. package/dist/ops/handlers.js +35 -3
  31. package/dist/ops/handlers.js.map +1 -1
  32. package/docs/API.md +1 -1
  33. package/docs/INTEGRATION.md +30 -2
  34. package/package.json +1 -1
  35. package/src/android/manifest.ts +11 -0
  36. package/src/android/meetSdkRemoteGradle.ts +125 -7
  37. package/src/config/meetSdkRemoteConfig.ts +13 -1
  38. package/src/ios/channelConfig.ts +18 -1
  39. package/src/ios/infoPlist.ts +0 -7
  40. package/src/ios/integrate.ts +177 -96
  41. package/src/ios/pbxprojEditor.ts +98 -16
  42. package/src/ops/fileStore.ts +7 -6
  43. package/src/ops/handlers.ts +38 -3
  44. package/tests/doctor.test.ts +43 -3
  45. package/tests/meetSdkRemoteConfig.test.ts +3 -2
  46. package/tests/meetSdkRemoteGradle.test.ts +2 -2
  47. package/tests/pipeline.android.test.ts +64 -10
  48. package/tests/pipeline.ios.test.ts +134 -43
  49. package/tests/platformSelection.test.ts +4 -4
@@ -25,14 +25,49 @@ import { loadMeetSdkDefaultConfigWithLatestAndroidVersion } from "../config/meet
25
25
  import { insertPermissions } from "../android/manifest.js";
26
26
  import type { TextFileStore } from "./fileStore.js";
27
27
 
28
- function ok(changed: string[], warnings: string[] = []): StepResult {
29
- return { ok: true, changedFiles: changed, warnings, errors: [] };
28
+ function ok(changed: string[], warnings: string[] = [], logs: string[] = []): StepResult {
29
+ return { ok: true, changedFiles: changed, logs, warnings, errors: [] };
30
30
  }
31
31
 
32
32
  function fail(errors: string[]): StepResult {
33
33
  return { ok: false, changedFiles: [], warnings: [], errors };
34
34
  }
35
35
 
36
+ function androidApplicationId(content: string): string | undefined {
37
+ const match = content.match(/^\s*applicationId\s+(?:=+\s*)?["']([^"']*)["']/m);
38
+ return match?.[1];
39
+ }
40
+
41
+ function androidResValues(content: string): Map<string, string> {
42
+ const out = new Map<string, string>();
43
+ const re = /resValue\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*(['"])(.*?)\3\s*\)/g;
44
+ for (const match of content.matchAll(re)) {
45
+ out.set(match[2], `${match[1]}:${match[4]}`);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function androidConfigChangeLogs(before: string, after: string): string[] {
51
+ const logs: string[] = [];
52
+ const beforeAppId = androidApplicationId(before);
53
+ const afterAppId = androidApplicationId(after);
54
+ if (afterAppId !== undefined && beforeAppId !== afterAppId) {
55
+ logs.push(`[meetgames] Android applicationId ${beforeAppId === undefined ? "<missing>" : beforeAppId} -> ${afterAppId}`);
56
+ }
57
+
58
+ const beforeRes = androidResValues(before);
59
+ const afterRes = androidResValues(after);
60
+ for (const [key, next] of afterRes) {
61
+ const prev = beforeRes.get(key);
62
+ if (prev === undefined) {
63
+ logs.push(`[meetgames] Android resValue ${key} <missing> -> ${next}`);
64
+ } else if (prev !== next) {
65
+ logs.push(`[meetgames] Android resValue ${key} ${prev} -> ${next}`);
66
+ }
67
+ }
68
+ return logs;
69
+ }
70
+
36
71
  function readRootBuildGradle(projectRoot: string): { rel: string; abs: string } | null {
37
72
  const a = path.join(projectRoot, "build.gradle");
38
73
  if (fs.existsSync(a)) return { rel: "build.gradle", abs: a };
@@ -183,7 +218,7 @@ export const opHandlers: Record<string, OpHandler> = {
183
218
 
184
219
  store.write(modRel, modU.content);
185
220
  if (modU.changed) changed.push(modRel);
186
- return ok(changed, [...warnings, ...modU.warnings]);
221
+ return ok(changed, [...warnings, ...modU.warnings], androidConfigChangeLogs(modBefore, modU.content));
187
222
  },
188
223
 
189
224
  "gradle.insertRepositories": (ctx, store, args, _dry, _bc) => {
@@ -11,15 +11,55 @@ import { resolveIosSdkRootFromDirectory } from "../src/remote/sdkHomeDownload.js
11
11
 
12
12
  const pkgRoot = path.resolve(__dirname, "..");
13
13
  const androidLatestRoot = path.join(pkgRoot, "fixtures", "android-test-project", "android-latest-project");
14
- const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "tooltest");
15
- const iosRemoteConfigFixture = path.join(pkgRoot, "fixtures", "meetsdk-remote-config.ios-tooltest.json");
14
+ const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
16
15
  const hasIosProjectFixture = fs.existsSync(iosProjectRoot);
17
16
  const fixtureIosSdkRoot = resolveIosSdkRootFromDirectory(path.join(pkgRoot, "fixtures", "ios-sdk", "topSDK-ios--V1.6.0.5"));
18
17
 
19
18
  function copyIosProjectToTemp(): string {
20
19
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-doctor-"));
21
20
  fs.cpSync(iosProjectRoot, tmp, { recursive: true });
22
- fs.copyFileSync(iosRemoteConfigFixture, path.join(tmp, "meetsdk-remote-config.json"));
21
+ fs.writeFileSync(
22
+ path.join(tmp, "meetsdk-remote-config.json"),
23
+ JSON.stringify(
24
+ {
25
+ packageName: "com.meetgames.topsdk.demo",
26
+ channel: "APPLE",
27
+ devicePlatform: "ios",
28
+ topsdk: {
29
+ appId: "mock-ios-native-sample-app-id",
30
+ appSecret: "mock-ios-native-sample-app-secret-do-not-use-in-production",
31
+ },
32
+ sdkModules: {
33
+ login: {
34
+ guest: {},
35
+ facebook: {
36
+ clientId: "883695101201170",
37
+ scheme: "fb883695101201170",
38
+ secret: "f840b8663b1351ddcb8f6a640cee18c6",
39
+ name: "top-demo",
40
+ openMessenger: "0",
41
+ },
42
+ google: {
43
+ clientId: "396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql.apps.googleusercontent.com",
44
+ scheme: "com.googleusercontent.apps.396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql",
45
+ },
46
+ apple: {},
47
+ },
48
+ payment: { googleIap: {} },
49
+ analytics: {
50
+ appsflyer: {
51
+ devKey: "af-dev-key",
52
+ appleAppId: "123456789",
53
+ enableDebugLog: true,
54
+ },
55
+ },
56
+ },
57
+ },
58
+ null,
59
+ 2
60
+ ),
61
+ "utf8"
62
+ );
23
63
  return tmp;
24
64
  }
25
65
 
@@ -14,7 +14,7 @@ import { loadBuiltInMeetSdkDefaultConfig } from "../src/config/meetSdkDefaultCon
14
14
  const here = path.dirname(fileURLToPath(import.meta.url));
15
15
  const refPath = path.join(here, "..", "fixtures", "topsdk-config-reference.json");
16
16
  const offlineMockPath = path.join(here, "..", "fixtures", "meetsdk-remote-config.mock.json");
17
- const offlineIosMockPath = path.join(here, "..", "fixtures", "meetsdk-remote-config.ios-tooltest.json");
17
+ const offlineIosMockPath = path.join(here, "..", "fixtures", "meetsdk-remote-config.ios-native-sample.json");
18
18
  const downloadShapePath = path.join(here, "..", "fixtures", "meetsdk-remote-config.download-shape.json");
19
19
 
20
20
  const mapCtx = {
@@ -53,7 +53,7 @@ describe("meetSdkRemoteConfig", () => {
53
53
  naver: { clientId: "naver-client", secret: "naver-secret", name: "Naver", scheme: "naver-scheme" },
54
54
  apple: {},
55
55
  },
56
- payment: {},
56
+ payment: { appleIap: {} },
57
57
  analytics: {
58
58
  appsflyer: { devKey: "af-key", appleAppId: "1234567890" },
59
59
  firebase: { firebaseUrl: "https://cdn.example/GoogleService-Info.plist", firebaseName: "GoogleService-Info.plist" },
@@ -82,6 +82,7 @@ describe("meetSdkRemoteConfig", () => {
82
82
  devKey: "af-key",
83
83
  appleAppId: "1234567890",
84
84
  });
85
+ expect(parsed!.sdkModules.payment.appleIap).toMatchObject({});
85
86
  });
86
87
 
87
88
  it("does not treat legacy remote field names as valid config", () => {
@@ -150,7 +150,7 @@ android {
150
150
  expect(modOut.content.indexOf(TOPSDK_PLUGIN_AUTO_START)).toBeLessThan(modOut.content.indexOf("dependencies"));
151
151
  });
152
152
 
153
- it("updates apply plugin by id instead of duplicating", () => {
153
+ it("keeps an existing apply plugin with the same id instead of duplicating or rewriting", () => {
154
154
  const modIn = `
155
155
  apply plugin: 'com.android.application'
156
156
  apply plugin: 'com.google.gms.google-services'
@@ -173,7 +173,7 @@ dependencies { }
173
173
  .split("\n")
174
174
  .filter((l) => l.includes("com.google.gms.google-services"));
175
175
  expect(pluginLines.length).toBe(1);
176
- expect(modOut.content).toContain(TOPSDK_PLUGIN_AUTO_START);
176
+ expect(modOut.content).not.toContain(TOPSDK_PLUGIN_AUTO_START);
177
177
  });
178
178
 
179
179
  it("updates classpath by group:artifact instead of duplicating", () => {
@@ -40,7 +40,10 @@ function testProjectRoot(...names: string[]): string {
40
40
  return path.join(androidFixtureRoot, names[0] ?? "");
41
41
  }
42
42
 
43
- async function runAndroidPipelineDryRun(projectRoot: string, opts?: { requirePackageNameInPatch?: boolean }) {
43
+ async function runAndroidPipelineDryRun(
44
+ projectRoot: string,
45
+ opts?: { requirePackageNameInPatch?: boolean; requireRemoteValuesInPatch?: boolean; allowEmptyPatch?: boolean }
46
+ ) {
44
47
  const manifest = loadManifestFile(path.join(recipeFixtureRoot, "android-default.fixture.yaml"));
45
48
  const cfg = JSON.parse(fs.readFileSync(path.join(projectRoot, "meetsdk-remote-config.json"), "utf8")) as {
46
49
  packageName: string;
@@ -51,20 +54,32 @@ async function runAndroidPipelineDryRun(projectRoot: string, opts?: { requirePac
51
54
  expect(ctx.android?.ok).toBe(true);
52
55
  const { report, patch, binaryCopies } = await runPipeline(ctx, manifest, { dryRun: true });
53
56
  expect(report.errors).toEqual([]);
54
- expect(patch.length).toBeGreaterThan(0);
57
+ if (!opts?.allowEmptyPatch) {
58
+ expect(patch.length).toBeGreaterThan(0);
59
+ }
55
60
  expect(binaryCopies).toEqual([]);
56
- expect(patch).toContain("TOPSDK REPO AUTO");
57
- expect(patch).toContain("TOPSDK AUTO");
61
+ if (!opts?.allowEmptyPatch || patch.length > 0) {
62
+ expect(patch).toContain("TOPSDK REPO AUTO");
63
+ expect(patch).toContain("TOPSDK AUTO");
64
+ }
58
65
  if (opts?.requirePackageNameInPatch !== false) {
59
66
  expect(patch).toContain(cfg.packageName);
60
67
  }
61
- expect(patch).toContain(cfg.topsdk.appId);
62
- expect(patch).toContain(cfg.channel);
63
- expect(patch).toContain("facebook_app_id");
64
- expect(patch).toContain("top_app_id");
68
+ if (opts?.requireRemoteValuesInPatch !== false) {
69
+ expect(patch).toContain(cfg.topsdk.appId);
70
+ expect(patch).toContain(cfg.channel);
71
+ expect(patch).toContain("facebook_app_id");
72
+ expect(patch).toContain("top_app_id");
73
+ }
65
74
  return { ctx, patch };
66
75
  }
67
76
 
77
+ function copyAndroidLatestToTemp(): string {
78
+ const tmp = fs.mkdtempSync(path.join(fs.realpathSync("/tmp"), "meet-android-idempotent-"));
79
+ fs.cpSync(androidLatestRoot, tmp, { recursive: true });
80
+ return tmp;
81
+ }
82
+
68
83
  describe("pipeline android fixture", () => {
69
84
  beforeEach(() => {
70
85
  stubSdkHomeVersion();
@@ -85,11 +100,50 @@ describe("pipeline android fixture", () => {
85
100
  const hasPowerRaidConfig = fs.existsSync(path.join(powerRaidRoot, "meetsdk-remote-config.json"));
86
101
 
87
102
  it.skipIf(!hasPowerRaidConfig)("dry-run on power-raid (:launcher)", async () => {
88
- const { ctx, patch } = await runAndroidPipelineDryRun(powerRaidRoot, { requirePackageNameInPatch: false });
103
+ const { ctx, patch } = await runAndroidPipelineDryRun(powerRaidRoot, {
104
+ requirePackageNameInPatch: false,
105
+ requireRemoteValuesInPatch: false,
106
+ allowEmptyPatch: true,
107
+ });
89
108
  expect(ctx.android?.ok && ctx.android.moduleName).toBe(":launcher");
90
- expect(patch).toContain("launcher/build.gradle");
91
109
  const addedLines = patch.split("\n").filter((line) => line.startsWith("+")).join("\n");
92
110
  expect(addedLines).not.toContain("//Firebase");
93
111
  expect(addedLines).not.toContain("//Appsflyer");
94
112
  });
113
+
114
+ it("apply is idempotent and logs Android value updates", async () => {
115
+ const tmp = copyAndroidLatestToTemp();
116
+ try {
117
+ const manifest = loadManifestFile(path.join(recipeFixtureRoot, "android-default.fixture.yaml"));
118
+ const buildGradlePath = path.join(tmp, "app", "build.gradle");
119
+ const beforeGradle = fs.readFileSync(buildGradlePath, "utf8");
120
+ fs.writeFileSync(
121
+ buildGradlePath,
122
+ beforeGradle.replace(
123
+ 'versionName "1.0"',
124
+ `versionName "1.0"
125
+ resValue('string', 'top_app_id', 'old-app-id')
126
+ resValue('string', 'facebook_app_id', 'old-facebook-id')`
127
+ ),
128
+ "utf8"
129
+ );
130
+
131
+ const ctx = buildWorkspaceContext(tmp, pkgRoot);
132
+ const first = await runPipeline(ctx, manifest, { dryRun: false });
133
+ expect(first.report.errors).toEqual([]);
134
+ const logs = (first.report.logs ?? []).join("\n");
135
+ expect(logs).toContain("Android applicationId com.example.myapplication -> com.meet.integrate.androidsample");
136
+ expect(logs).toContain("Android resValue top_app_id string:old-app-id -> string:mock-topsdk-app-id");
137
+ expect(logs).toContain("Android resValue facebook_app_id string:old-facebook-id -> string:0000000000000000");
138
+
139
+ const second = await runPipeline(buildWorkspaceContext(tmp, pkgRoot), manifest, { dryRun: false });
140
+ expect(second.report.errors).toEqual([]);
141
+ const after = fs.readFileSync(buildGradlePath, "utf8");
142
+ expect((after.match(/resValue\('string', 'top_app_id'/g) ?? []).length).toBe(1);
143
+ expect((after.match(/resValue\('string', 'facebook_app_id'/g) ?? []).length).toBe(1);
144
+ expect((after.match(/applicationId "com.meet.integrate.androidsample"/g) ?? []).length).toBe(1);
145
+ } finally {
146
+ fs.rmSync(tmp, { recursive: true, force: true });
147
+ }
148
+ });
95
149
  });
@@ -7,32 +7,19 @@ import { loadManifestFile } from "../src/config/loadManifest.js";
7
7
  import { buildWorkspaceContext } from "../src/core/workspace.js";
8
8
  import { runPipeline } from "../src/core/pipeline.js";
9
9
  import { detectIOS } from "../src/ios/detect.js";
10
+ import { parsePlistXml } from "../src/ios/infoPlist.js";
10
11
  import { resolveIosSdkRootFromDirectory } from "../src/remote/sdkHomeDownload.js";
11
12
 
12
13
  const here = path.dirname(fileURLToPath(import.meta.url));
13
14
  const pkgRoot = path.resolve(here, "..");
14
- const iosRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "tooltest");
15
- const nativeSampleRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
16
- const iosRemoteConfigFixture = path.join(pkgRoot, "fixtures", "meetsdk-remote-config.ios-tooltest.json");
15
+ const iosRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
17
16
  const hasIosProjectFixture = fs.existsSync(iosRoot);
18
- const hasNativeSampleFixture = fs.existsSync(nativeSampleRoot);
19
17
  const manifest = () => loadManifestFile(path.join(pkgRoot, "recipes", "ios-default.yaml"));
20
18
  const fixtureIosSdkRoot = resolveIosSdkRootFromDirectory(path.join(pkgRoot, "fixtures", "ios-sdk", "topSDK-ios--V1.6.0.5"));
21
19
 
22
- function writeOfflineRemoteConfig(projectRoot: string): void {
23
- fs.copyFileSync(iosRemoteConfigFixture, path.join(projectRoot, "meetsdk-remote-config.json"));
24
- }
25
-
26
20
  function copyProjectToTemp(): string {
27
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-integrate-"));
28
- fs.cpSync(iosRoot, tmp, { recursive: true });
29
- writeOfflineRemoteConfig(tmp);
30
- return tmp;
31
- }
32
-
33
- function copyNativeSampleToTemp(): string {
34
21
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-native-sample-"));
35
- fs.cpSync(nativeSampleRoot, tmp, { recursive: true });
22
+ fs.cpSync(iosRoot, tmp, { recursive: true });
36
23
  writeNativeSampleRemoteConfig(tmp);
37
24
  return tmp;
38
25
  }
@@ -63,7 +50,7 @@ function writeNativeSampleRemoteConfig(projectRoot: string): void {
63
50
  apple: {},
64
51
  },
65
52
  payment: {
66
- googleIap: {},
53
+ appleIap: {},
67
54
  },
68
55
  analytics: {
69
56
  appsflyer: {
@@ -97,6 +84,14 @@ function plannedIosAsset(patch: string, binaryCopies: Array<{ relTo: string }>,
97
84
  return patch.includes(name) || binaryCopies.some((c) => c.relTo.includes(name));
98
85
  }
99
86
 
87
+ function occurrenceCount(content: string, needle: string): number {
88
+ return content.split(needle).length - 1;
89
+ }
90
+
91
+ function regexCount(content: string, pattern: RegExp): number {
92
+ return [...content.matchAll(pattern)].length;
93
+ }
94
+
100
95
  function testIosContext(projectRoot: string) {
101
96
  return buildWorkspaceContext(projectRoot, pkgRoot, { iosSdkRoot: fixtureIosSdkRoot });
102
97
  }
@@ -109,29 +104,29 @@ describe("pipeline ios fixture SDK", () => {
109
104
  });
110
105
 
111
106
  describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
112
- it("detects tooltest xcodeproj with AppDelegate target", () => {
107
+ it("detects native-sample xcodeproj with AppDelegate target", () => {
113
108
  const d = detectIOS(iosRoot);
114
109
  expect(d.ok).toBe(true);
115
110
  if (d.ok) {
116
- expect(d.targetName).toBe("tooltest");
117
- expect(fs.existsSync(path.join(iosRoot, "tooltest", "AppDelegate.m"))).toBe(true);
111
+ expect(d.targetName).toBe("native-sample");
112
+ expect(fs.existsSync(path.join(iosRoot, "native-sample", "AppDelegate.m"))).toBe(true);
118
113
  }
119
114
  });
120
115
 
121
116
  it("detects when project root is the xcodeproj directory itself", () => {
122
- const xcodeprojRoot = path.join(iosRoot, "tooltest.xcodeproj");
117
+ const xcodeprojRoot = path.join(iosRoot, "native-sample.xcodeproj");
123
118
  const d = detectIOS(xcodeprojRoot);
124
119
  expect(d.ok).toBe(true);
125
120
  if (d.ok) {
126
121
  expect(d.xcodeprojPath).toBe(xcodeprojRoot);
127
- expect(d.targetName).toBe("tooltest");
122
+ expect(d.targetName).toBe("native-sample");
128
123
  }
129
124
  });
130
125
 
131
126
  it("builds workspace context when project root is the xcodeproj directory", () => {
132
- const ctx = testIosContext(path.join(iosRoot, "tooltest.xcodeproj"));
127
+ const ctx = testIosContext(path.join(iosRoot, "native-sample.xcodeproj"));
133
128
  expect(ctx.ios?.ok).toBe(true);
134
- expect(ctx.ios?.ok && ctx.ios.xcodeprojPath).toBe(path.join(iosRoot, "tooltest.xcodeproj"));
129
+ expect(ctx.ios?.ok && ctx.ios.xcodeprojPath).toBe(path.join(iosRoot, "native-sample.xcodeproj"));
135
130
  });
136
131
 
137
132
  it("dry-run: plans TOPCore + GuestSignin frameworks and pbxproj edits", async () => {
@@ -161,28 +156,92 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
161
156
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPCore.framework"))).toBe(true);
162
157
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPSDK.framework"))).toBe(true);
163
158
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPGuestSigninPlugin.framework"))).toBe(true);
159
+ expect(fs.existsSync(path.join(tmp, "topSDK", "TOPIAPPayPlugin.framework"))).toBe(true);
160
+ expect(fs.readFileSync(path.join(tmp, "topSDK", "TopSDKInstall.swift"), "utf8")).toBe("");
164
161
  expect(fs.existsSync(path.join(tmp, "topSDK", "TopSDKSource.bundle"))).toBe(true);
165
162
 
166
- const pbx = fs.readFileSync(path.join(tmp, "tooltest.xcodeproj", "project.pbxproj"), "utf8");
163
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
167
164
  expect(pbx).toContain("TOPCore.framework");
168
165
  expect(pbx).toContain("TOPGuestSigninPlugin.framework");
166
+ expect(pbx).toContain("TOPIAPPayPlugin.framework in Frameworks");
167
+ expect(pbx).toContain("TOPCoreModel.xcdatamodeld in Sources");
168
+ expect(pbx).not.toContain("TOPCoreModel.xcdatamodeld in Resources");
169
+ expect(pbx).not.toContain("FBSDKLoginKit.xcframework in Resources");
170
+ expect(pbx).toContain("LIBRARY_SEARCH_PATHS = \"$(SRCROOT)/topSDK\"");
171
+ expect(pbx).toContain("SWIFT_VERSION = 5.0");
169
172
  expect(pbx).toContain('"-ObjC"');
170
- expect(pbx).toContain('"-lc++"');
173
+ expect(pbx).toContain("libc++.tbd");
171
174
  expect(pbx).not.toMatch(/\n\s+-ObjC,/);
172
- expect(pbx).not.toMatch(/\n\s+-lc\+\+,/);
173
175
  expect(pbxBuildFileRefErrors(pbx).filter((e) => /TOP|FBSDK|Google|AppsFlyer|TopSDKInstall/.test(e))).toEqual([]);
174
176
  expect(pbx).not.toMatch(/\bundefined;/);
175
177
 
176
- const delegate = fs.readFileSync(path.join(tmp, "tooltest", "AppDelegate.m"), "utf8");
178
+ const delegate = fs.readFileSync(path.join(tmp, "native-sample", "AppDelegate.m"), "utf8");
177
179
  expect(delegate).toContain("#import <TOPSDK/TopSDK.h>");
178
180
  expect(delegate).toContain("TopSDK.sharedInstance");
179
181
  expect(delegate).toContain("TOPDataSDK");
180
182
 
181
- const plist = fs.readFileSync(path.join(tmp, "tooltest", "Info.plist"), "utf8");
183
+ const plist = fs.readFileSync(path.join(tmp, "native-sample", "Info.plist"), "utf8");
182
184
  expect(plist).toContain("<key>TOPSDK</key>");
183
185
  expect(plist).toContain("<key>APP_ID</key>");
184
- expect(plist).toContain("mock-ios-tooltest-app-id");
186
+ expect(plist).toContain("mock-ios-native-sample-app-id");
185
187
  expect(plist).toContain("GuestSignin");
188
+ expect(plist).toContain("IAPPay");
189
+ expect(plist).not.toContain("NSAppTransportSecurity");
190
+ } finally {
191
+ fs.rmSync(tmp, { recursive: true, force: true });
192
+ }
193
+ });
194
+
195
+ it("apply: is idempotent and logs iOS value updates", async () => {
196
+ const tmp = copyProjectToTemp();
197
+ try {
198
+ const plistPath = path.join(tmp, "native-sample", "Info.plist");
199
+ const beforePlist = fs.readFileSync(plistPath, "utf8");
200
+ fs.writeFileSync(
201
+ plistPath,
202
+ beforePlist
203
+ .replace("<key>FacebookAppID</key>\n\t<string>883695101201170</string>", "<key>FacebookAppID</key>\n\t<string>old-facebook-app-id</string>")
204
+ .replace(
205
+ "</dict>\n</plist>",
206
+ "\t<key>LSApplicationQueriesSchemes</key>\n\t<array>\n\t\t<string>fbapi</string>\n\t</array>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Editor</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>fb883695101201170</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>"
207
+ ),
208
+ "utf8"
209
+ );
210
+
211
+ const first = await runPipeline(testIosContext(tmp), manifest(), { dryRun: false });
212
+ expect(first.report.errors).toEqual([]);
213
+ const firstLogs = (first.report.logs ?? []).join("\n");
214
+ expect(firstLogs).toContain("SUCCESS: info.plist配置key:FacebookAppID old-facebook-app-id -> 883695101201170");
215
+
216
+ const second = await runPipeline(testIosContext(tmp), manifest(), { dryRun: false });
217
+ expect(second.report.errors).toEqual([]);
218
+ const secondLogs = (second.report.logs ?? []).join("\n");
219
+ expect(secondLogs).toContain("SUCCESS: info.plist配置key:FacebookAppID 未变化 value:883695101201170");
220
+ expect(secondLogs).toContain("SUCCESS: BuildSetting设置VALIDATE_WORKSPACE 未变化 value:YES");
221
+
222
+ const plist = fs.readFileSync(plistPath, "utf8");
223
+ const plistData = parsePlistXml(plist);
224
+ const queries = Array.isArray(plistData.LSApplicationQueriesSchemes)
225
+ ? (plistData.LSApplicationQueriesSchemes as string[])
226
+ : [];
227
+ const urlTypes = Array.isArray(plistData.CFBundleURLTypes)
228
+ ? (plistData.CFBundleURLTypes as Array<Record<string, unknown>>)
229
+ : [];
230
+ const urlSchemes = urlTypes.flatMap((entry) =>
231
+ Array.isArray(entry.CFBundleURLSchemes) ? (entry.CFBundleURLSchemes as string[]) : []
232
+ );
233
+ expect(occurrenceCount(plist, "<key>FacebookAppID</key>")).toBe(1);
234
+ expect(occurrenceCount(plist, "<key>FacebookClientToken</key>")).toBe(1);
235
+ expect(queries.filter((scheme) => scheme === "fbapi")).toHaveLength(1);
236
+ expect(urlSchemes.filter((scheme) => scheme === "fb883695101201170")).toHaveLength(1);
237
+ expect(urlSchemes.filter((scheme) => scheme === "com.googleusercontent.apps.396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql")).toHaveLength(1);
238
+ expect(occurrenceCount(plist, "<key>TOPSDK</key>")).toBe(1);
239
+
240
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
241
+ expect(regexCount(pbx, /\/\* TopSDKInstall\.swift in Sources \*\/ = \{isa = PBXBuildFile;/g)).toBe(1);
242
+ expect(regexCount(pbx, /\/\* TOPFacebookSigninPlugin\.framework in Frameworks \*\/ = \{isa = PBXBuildFile;/g)).toBe(1);
243
+ expect(regexCount(pbx, /\/\* TOPGoogleSigninPlugin\.framework in Frameworks \*\/ = \{isa = PBXBuildFile;/g)).toBe(1);
244
+ expect(occurrenceCount(pbx, "VALIDATE_WORKSPACE = YES")).toBe(2);
186
245
  } finally {
187
246
  fs.rmSync(tmp, { recursive: true, force: true });
188
247
  }
@@ -204,15 +263,15 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
204
263
  const { report } = await runPipeline(ctx, manifest(), { dryRun: false });
205
264
  expect(report.errors).toEqual([]);
206
265
 
207
- const entitlementsPath = path.join(tmp, "tooltest", "tooltest.entitlements");
266
+ const entitlementsPath = path.join(tmp, "native-sample", "native-sample.entitlements");
208
267
  expect(fs.existsSync(entitlementsPath)).toBe(true);
209
268
  const entitlements = fs.readFileSync(entitlementsPath, "utf8");
210
269
  expect(entitlements).toContain("com.apple.developer.applesignin");
211
270
  expect(entitlements).toContain("Default");
212
271
 
213
- const pbx = fs.readFileSync(path.join(tmp, "tooltest.xcodeproj", "project.pbxproj"), "utf8");
272
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
214
273
  expect(pbx).toContain("CODE_SIGN_ENTITLEMENTS");
215
- expect(pbx).toContain("tooltest/tooltest.entitlements");
274
+ expect(pbx).toContain("native-sample/native-sample.entitlements");
216
275
  expect(pbx).toContain("TOPAppleSigninPlugin.framework");
217
276
  } finally {
218
277
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -345,7 +404,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
345
404
  const { report, patch, binaryCopies } = await runPipeline(ctx, manifest(), { dryRun: true });
346
405
 
347
406
  expect(report.errors).toEqual([]);
348
- expect(report.warnings.join("\n")).toContain("would download GoogleService-Info.plist to tooltest/GoogleService-Info.plist");
407
+ expect(report.warnings.join("\n")).toContain("would download GoogleService-Info.plist to native-sample/GoogleService-Info.plist");
349
408
  expect(plannedIosAsset(patch, binaryCopies, "TOPDataFirebasePlugin.framework")).toBe(true);
350
409
  } finally {
351
410
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -356,7 +415,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
356
415
  const tmp = copyProjectToTemp();
357
416
  try {
358
417
  fs.writeFileSync(
359
- path.join(tmp, "tooltest", "AppDelegate.m"),
418
+ path.join(tmp, "native-sample", "AppDelegate.m"),
360
419
  `#import "AppDelegate.h"
361
420
 
362
421
  @implementation AppDelegate
@@ -370,7 +429,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
370
429
  "utf8"
371
430
  );
372
431
  fs.writeFileSync(
373
- path.join(tmp, "tooltest", "SceneDelegate.m"),
432
+ path.join(tmp, "native-sample", "SceneDelegate.m"),
374
433
  `#import "SceneDelegate.h"
375
434
 
376
435
  @implementation SceneDelegate
@@ -384,7 +443,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
384
443
  const { report } = await runPipeline(ctx, manifest(), { dryRun: false });
385
444
  expect(report.errors).toEqual([]);
386
445
 
387
- const appDelegate = fs.readFileSync(path.join(tmp, "tooltest", "AppDelegate.m"), "utf8");
446
+ const appDelegate = fs.readFileSync(path.join(tmp, "native-sample", "AppDelegate.m"), "utf8");
388
447
  expect(appDelegate).toContain("[TopSDK.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions]");
389
448
  expect(appDelegate).toContain("[TOPDataSDK application:application didFinishLaunchingWithOptions:launchOptions]");
390
449
  expect(appDelegate).not.toContain("openURL:url options");
@@ -392,7 +451,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
392
451
  expect(appDelegate).not.toContain("applicationDidEnterBackground");
393
452
  expect(appDelegate).not.toContain("applicationWillEnterForeground");
394
453
 
395
- const sceneDelegate = fs.readFileSync(path.join(tmp, "tooltest", "SceneDelegate.m"), "utf8");
454
+ const sceneDelegate = fs.readFileSync(path.join(tmp, "native-sample", "SceneDelegate.m"), "utf8");
396
455
  expect(sceneDelegate).toContain("[TopSDK.sharedInstance scene:scene openURLContexts:URLContexts]");
397
456
  expect(sceneDelegate).toContain("[TopSDK.sharedInstance scene:scene continueUserActivity:userActivity]");
398
457
  expect(sceneDelegate).toContain("[TopSDK.sharedInstance sceneWillEnterForeground:scene]");
@@ -405,12 +464,12 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
405
464
  it("fails when the configured Info.plist file is missing", async () => {
406
465
  const tmp = copyProjectToTemp();
407
466
  try {
408
- fs.rmSync(path.join(tmp, "tooltest", "Info.plist"));
467
+ fs.rmSync(path.join(tmp, "native-sample", "Info.plist"));
409
468
 
410
469
  const ctx = testIosContext(tmp);
411
470
  const { report } = await runPipeline(ctx, manifest(), { dryRun: true });
412
471
 
413
- expect(report.errors.join("\n")).toContain("Info.plist not found for iOS target tooltest");
472
+ expect(report.errors.join("\n")).toContain("Info.plist not found for iOS target native-sample");
414
473
  } finally {
415
474
  fs.rmSync(tmp, { recursive: true, force: true });
416
475
  }
@@ -440,9 +499,9 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
440
499
  });
441
500
  });
442
501
 
443
- describe.skipIf(!hasNativeSampleFixture)("pipeline ios native-sample parity", () => {
502
+ describe.skipIf(!hasIosProjectFixture)("pipeline ios native-sample parity", () => {
444
503
  it("apply: mirrors topsdk-tool-ios logs and integration points for native-sample", async () => {
445
- const tmp = copyNativeSampleToTemp();
504
+ const tmp = copyProjectToTemp();
446
505
  try {
447
506
  const ctx = testIosContext(tmp);
448
507
  const { report } = await runPipeline(ctx, manifest(), { dryRun: false });
@@ -461,13 +520,45 @@ describe.skipIf(!hasNativeSampleFixture)("pipeline ios native-sample parity", ()
461
520
  expect(logs).toContain("检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口");
462
521
  expect(logs).toContain("SUCCESS: info.plist配置写入完成");
463
522
  expect(logs).toContain("!! 接入流程已全部结束 !!");
523
+ const orderedLogMarkers = [
524
+ "- 插件:GuestSignin, 类型:1,版本:1.6.0.3",
525
+ "SUCCESS: 插件GuestSignin接入完成",
526
+ "- 插件:UI, 类型:3,版本:1.6.0.3",
527
+ "SUCCESS: 插件UI接入完成",
528
+ "- 插件:IAPPay, 类型:2,版本:1.6.0.3",
529
+ "SUCCESS: 插件IAPPay接入完成",
530
+ "- 插件:AppsFlyerManager, 类型:4,版本:1.6.0.3",
531
+ "SUCCESS: info.plist添加配置key:NSAdvertisingAttributionReportEndpoint value:https://appsflyer-skadnetwork.com/ 完成",
532
+ "SUCCESS: 插件AppsFlyerManager接入完成",
533
+ "- 插件:FacebookSignin, 类型:1,版本:1.6.0.3",
534
+ "SUCCESS: 未检测到Swift代码文件,自动添加TopSDKInstall.swift完成",
535
+ "SUCCESS: 添加queriesSchemes:fbapi 完成",
536
+ "SUCCESS: 设置urlScheme:fb883695101201170完成",
537
+ "SUCCESS: 插件FacebookSignin接入完成",
538
+ "- 插件:GoogleSignin, 类型:1,版本:1.6.0.3",
539
+ "SUCCESS: 插件GoogleSignin接入完成",
540
+ "- 插件:AppleSignin, 类型:1,版本:1.6.0.3",
541
+ "SUCCESS: 启用SigninWithApple完成",
542
+ "SUCCESS: 插件AppleSignin接入完成",
543
+ "- 插件:TOPSDK, 类型:0,版本:1.6.0.3",
544
+ "检测到工程UIApplicationDelegate实现类,将自动接入对应接口",
545
+ "检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口",
546
+ "SUCCESS: 插件TOPSDK接入完成",
547
+ "SUCCESS: info.plist配置写入完成",
548
+ ];
549
+ let lastLogIndex = -1;
550
+ for (const marker of orderedLogMarkers) {
551
+ const index = logs.indexOf(marker);
552
+ expect(index, marker).toBeGreaterThan(lastLogIndex);
553
+ lastLogIndex = index;
554
+ }
464
555
 
465
556
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPUIPlugin.framework"))).toBe(true);
466
557
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPFacebookSigninPlugin.framework"))).toBe(true);
467
558
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPGoogleSigninPlugin.framework"))).toBe(true);
468
559
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPAppleSigninPlugin.framework"))).toBe(true);
469
560
  expect(fs.existsSync(path.join(tmp, "topSDK", "TopSDKSource.bundle"))).toBe(true);
470
- expect(fs.existsSync(path.join(tmp, "native-sample", "TopSDKInstall.swift"))).toBe(true);
561
+ expect(fs.readFileSync(path.join(tmp, "topSDK", "TopSDKInstall.swift"), "utf8")).toBe("");
471
562
 
472
563
  const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
473
564
  expect(pbx).toContain("TOPUIPlugin.framework");
@@ -11,7 +11,7 @@ import type { Manifest } from "../src/contracts/types.js";
11
11
 
12
12
  const pkgRoot = path.resolve(__dirname, "..");
13
13
  const androidLatestRoot = path.join(pkgRoot, "fixtures", "android-test-project", "android-latest-project");
14
- const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "tooltest");
14
+ const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
15
15
  const hasIosProjectFixture = fs.existsSync(iosProjectRoot);
16
16
 
17
17
  describe("platform selection", () => {
@@ -58,11 +58,11 @@ describe("platform selection", () => {
58
58
 
59
59
  it.skipIf(!hasIosProjectFixture)("overrides detected iOS target name when explicitly provided", () => {
60
60
  const ctx = buildWorkspaceContext(iosProjectRoot, pkgRoot, {
61
- appTarget: "tooltest",
61
+ appTarget: "native-sample",
62
62
  });
63
63
 
64
- expect(ctx.ios?.ok && ctx.ios.targetName).toBe("tooltest");
65
- expect(ctx.ios?.ok && ctx.ios.targetNames).toContain("tooltest");
64
+ expect(ctx.ios?.ok && ctx.ios.targetName).toBe("native-sample");
65
+ expect(ctx.ios?.ok && ctx.ios.targetNames).toContain("native-sample");
66
66
  });
67
67
 
68
68
  it.skipIf(!hasIosProjectFixture)("rejects missing iOS app target names during detection", () => {